feat(web): options to disable machine

This commit is contained in:
BennyKok 2023-12-21 13:39:36 +08:00
parent eda922a5e3
commit 61d3e2f65a
10 changed files with 744 additions and 55 deletions

View File

@ -0,0 +1 @@
ALTER TABLE "comfyui_deploy"."machines" ADD COLUMN "disabled" boolean DEFAULT false NOT NULL;

View File

@ -0,0 +1,642 @@
{
"id": "2e7e5e52-84ad-46fc-a899-fd9464995b9c",
"prevId": "d0939fc6-9073-4277-8406-ec609494b507",
"version": "5",
"dialect": "pg",
"tables": {
"api_keys": {
"name": "api_keys",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"revoked": {
"name": "revoked",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"api_keys_user_id_users_id_fk": {
"name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"api_keys_key_unique": {
"name": "api_keys_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
}
}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"environment": {
"name": "environment",
"type": "deployment_environment",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"deployments_user_id_users_id_fk": {
"name": "deployments_user_id_users_id_fk",
"tableFrom": "deployments",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_workflow_version_id_workflow_versions_id_fk": {
"name": "deployments_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "deployments",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"deployments_workflow_id_workflows_id_fk": {
"name": "deployments_workflow_id_workflows_id_fk",
"tableFrom": "deployments",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"deployments_machine_id_machines_id_fk": {
"name": "deployments_machine_id_machines_id_fk",
"tableFrom": "deployments",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"machines": {
"name": "machines",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"endpoint": {
"name": "endpoint",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
"foreignKeys": {
"machines_user_id_users_id_fk": {
"name": "machines_user_id_users_id_fk",
"tableFrom": "machines",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_run_outputs": {
"name": "workflow_run_outputs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_run_outputs_run_id_workflow_runs_id_fk": {
"name": "workflow_run_outputs_run_id_workflow_runs_id_fk",
"tableFrom": "workflow_run_outputs",
"tableTo": "workflow_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_runs": {
"name": "workflow_runs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"workflow_inputs": {
"name": "workflow_inputs",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "workflow_run_origin",
"primaryKey": false,
"notNull": true,
"default": "'api'"
},
"status": {
"name": "status",
"type": "workflow_run_status",
"primaryKey": false,
"notNull": true,
"default": "'not-started'"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_runs_workflow_version_id_workflow_versions_id_fk": {
"name": "workflow_runs_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"workflow_runs_workflow_id_workflows_id_fk": {
"name": "workflow_runs_workflow_id_workflows_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"workflow_runs_machine_id_machines_id_fk": {
"name": "workflow_runs_machine_id_machines_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflows": {
"name": "workflows",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflows_user_id_users_id_fk": {
"name": "workflows_user_id_users_id_fk",
"tableFrom": "workflows",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_versions": {
"name": "workflow_versions",
"schema": "comfyui_deploy",
"columns": {
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow": {
"name": "workflow",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_api": {
"name": "workflow_api",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_versions_workflow_id_workflows_id_fk": {
"name": "workflow_versions_workflow_id_workflows_id_fk",
"tableFrom": "workflow_versions",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"deployment_environment": {
"name": "deployment_environment",
"values": {
"staging": "staging",
"production": "production"
}
},
"workflow_run_origin": {
"name": "workflow_run_origin",
"values": {
"manual": "manual",
"api": "api"
}
},
"workflow_run_status": {
"name": "workflow_run_status",
"values": {
"not-started": "not-started",
"running": "running",
"success": "success",
"failed": "failed"
}
}
},
"schemas": {
"comfyui_deploy": "comfyui_deploy"
},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -85,6 +85,13 @@
"when": 1702960221042, "when": 1702960221042,
"tag": "0011_peaceful_william_stryker", "tag": "0011_peaceful_william_stryker",
"breakpoints": true "breakpoints": true
},
{
"idx": 12,
"version": "5",
"when": 1703136669370,
"tag": "0012_exotic_sumo",
"breakpoints": true
} }
] ]
} }

View File

@ -15,7 +15,7 @@ async function MachineListServer() {
return <div>No auth</div>; return <div>No auth</div>;
} }
const workflow = await db.query.machinesTable.findMany({ const machines = await db.query.machinesTable.findMany({
orderBy: desc(machinesTable.updated_at), orderBy: desc(machinesTable.updated_at),
where: eq(machinesTable.user_id, userId), where: eq(machinesTable.user_id, userId),
}); });
@ -23,16 +23,7 @@ async function MachineListServer() {
return ( return (
<div className="w-full"> <div className="w-full">
{/* <div>Machines</div> */} {/* <div>Machines</div> */}
<MachineList <MachineList data={machines} />
data={workflow.map((x) => {
return {
id: x.id,
name: x.name,
date: x.updated_at,
endpoint: x.endpoint,
};
})}
/>
</div> </div>
); );
} }

View File

@ -11,6 +11,7 @@ import {
FormMessage, FormMessage,
Form, Form,
} from "./ui/form"; } from "./ui/form";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { import {
@ -38,7 +39,13 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { addMachine, deleteMachine } from "@/server/curdMachine"; import type { MachineType } from "@/db/schema";
import {
addMachine,
deleteMachine,
disableMachine,
enableMachine,
} from "@/server/curdMachine";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import type { import type {
ColumnDef, ColumnDef,
@ -59,12 +66,7 @@ import * as React from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
export type Machine = { export type Machine = MachineType;
id: string;
name: string;
endpoint: string;
date: Date;
};
export const columns: ColumnDef<Machine>[] = [ export const columns: ColumnDef<Machine>[] = [
{ {
@ -106,7 +108,12 @@ export const columns: ColumnDef<Machine>[] = [
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
// <a className="hover:underline" href={`/${row.original.id}`}> // <a className="hover:underline" href={`/${row.original.id}`}>
row.getValue("name") <div className="flex flex-row gap-2">
<div>{row.getValue("name")}</div>
{row.original.disabled && (
<Badge variant="destructive">Disabled</Badge>
)}
</div>
// </a> // </a>
); );
}, },
@ -137,7 +144,7 @@ export const columns: ColumnDef<Machine>[] = [
}, },
cell: ({ row }) => ( cell: ({ row }) => (
<div className="capitalize text-right"> <div className="capitalize text-right">
{getRelativeTime(row.original.date)} {getRelativeTime(row.original.updated_at)}
</div> </div>
), ),
}, },
@ -146,7 +153,7 @@ export const columns: ColumnDef<Machine>[] = [
id: "actions", id: "actions",
enableHiding: false, enableHiding: false,
cell: ({ row }) => { cell: ({ row }) => {
const workflow = row.original; const machine = row.original;
return ( return (
<DropdownMenu> <DropdownMenu>
@ -161,11 +168,29 @@ export const columns: ColumnDef<Machine>[] = [
<DropdownMenuItem <DropdownMenuItem
className="text-destructive" className="text-destructive"
onClick={async () => { onClick={async () => {
callServerPromise(deleteMachine(workflow.id)); callServerPromise(deleteMachine(machine.id));
}} }}
> >
Delete Machine Delete Machine
</DropdownMenuItem> </DropdownMenuItem>
{machine.disabled ? (
<DropdownMenuItem
onClick={async () => {
callServerPromise(enableMachine(machine.id));
}}
>
Enable Machine
</DropdownMenuItem>
) : (
<DropdownMenuItem
className="text-destructive"
onClick={async () => {
callServerPromise(disableMachine(machine.id));
}}
>
Disable Machine
</DropdownMenuItem>
)}
{/* <DropdownMenuSeparator /> {/* <DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem> <DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem> */} <DropdownMenuItem>View payment details</DropdownMenuItem> */}

View File

@ -176,6 +176,7 @@ export const machinesTable = dbSchema.table("machines", {
endpoint: text("endpoint").notNull(), endpoint: text("endpoint").notNull(),
created_at: timestamp("created_at").defaultNow().notNull(), created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(), updated_at: timestamp("updated_at").defaultNow().notNull(),
disabled: boolean("disabled").default(false).notNull(),
}); });
export const deploymentsTable = dbSchema.table("deployments", { export const deploymentsTable = dbSchema.table("deployments", {
@ -227,3 +228,4 @@ export const apiKeyTable = dbSchema.table("api_keys", {
export type UserType = InferSelectModel<typeof usersTable>; export type UserType = InferSelectModel<typeof usersTable>;
export type WorkflowType = InferSelectModel<typeof workflowTable>; export type WorkflowType = InferSelectModel<typeof workflowTable>;
export type MachineType = InferSelectModel<typeof machinesTable>;

View File

@ -1,12 +1,12 @@
"use server"; "use server";
import { withServerPromise } from "./withServerPromise";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { workflowRunsTable } from "@/db/schema"; import { machinesTable, workflowRunsTable } from "@/db/schema";
import { ComfyAPI_Run } from "@/types/ComfyAPI_Run"; import { ComfyAPI_Run } from "@/types/ComfyAPI_Run";
import { eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import "server-only"; import "server-only";
import { withServerPromise } from "./withServerPromise";
export const createRun = withServerPromise( export const createRun = withServerPromise(
async ( async (
@ -14,20 +14,24 @@ export const createRun = withServerPromise(
workflow_version_id: string, workflow_version_id: string,
machine_id: string, machine_id: string,
inputs?: Record<string, string>, inputs?: Record<string, string>,
isManualRun?: boolean, isManualRun?: boolean
) => { ) => {
const machine = await db.query.machinesTable.findFirst({ const machine = await db.query.machinesTable.findFirst({
where: eq(workflowRunsTable.id, machine_id), where: and(
eq(machinesTable.id, machine_id),
eq(machinesTable.disabled, false)
),
}); });
if (!machine) { if (!machine) {
throw new Error("Machine not found"); throw new Error("Machine not found");
} }
const workflow_version_data = const workflow_version_data = await db.query.workflowVersionTable.findFirst(
await db.query.workflowVersionTable.findFirst({ {
where: eq(workflowRunsTable.id, workflow_version_id), where: eq(workflowRunsTable.id, workflow_version_id),
}); }
);
if (!workflow_version_data) { if (!workflow_version_data) {
throw new Error("Workflow version not found"); throw new Error("Workflow version not found");
@ -79,7 +83,7 @@ export const createRun = withServerPromise(
workflow_version_id: workflow_version_data.id, workflow_version_id: workflow_version_data.id,
workflow_inputs: inputs, workflow_inputs: inputs,
machine_id, machine_id,
origin: isManualRun ? "manual" : "api" origin: isManualRun ? "manual" : "api",
}) })
.returning(); .returning();
@ -89,5 +93,5 @@ export const createRun = withServerPromise(
workflow_run_id: workflow_run[0].id, workflow_run_id: workflow_run[0].id,
message: "Successful workflow run", message: "Successful workflow run",
}; };
}, }
); );

View File

@ -1,30 +1,22 @@
"use server"; "use server";
import { withServerPromise } from "./withServerPromise";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { machinesTable } from "@/db/schema"; import { machinesTable } from "@/db/schema";
import { auth } from "@clerk/nextjs"; import { auth } from "@clerk/nextjs";
import { eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import "server-only"; import "server-only";
// export async function addMachine(form: FormData) {
// const name = form.get("name") as string;
// const endpoint = form.get("endpoint") as string;
// await db.insert(machinesTable).values({
// name,
// endpoint,
// });
// revalidatePath("/machines");
// }
export async function getMachines() { export async function getMachines() {
const { userId } = auth(); const { userId } = auth();
if (!userId) throw new Error("No user id"); if (!userId) throw new Error("No user id");
const machines = await db const machines = await db
.select() .select()
.from(machinesTable) .from(machinesTable)
.where(eq(machinesTable.user_id, userId)); .where(
and(eq(machinesTable.user_id, userId), eq(machinesTable.disabled, false))
);
return machines; return machines;
} }
@ -40,10 +32,36 @@ export async function addMachine(name: string, endpoint: string) {
revalidatePath("/machines"); revalidatePath("/machines");
} }
export async function deleteMachine( export const deleteMachine = withServerPromise(
machine_id: string async (machine_id: string): Promise<{ message: string }> => {
): Promise<{ message: string; error?: boolean }> { await db.delete(machinesTable).where(eq(machinesTable.id, machine_id));
await db.delete(machinesTable).where(eq(machinesTable.id, machine_id)); revalidatePath("/machines");
revalidatePath("/machines"); return { message: "Machine Deleted" };
return { message: "Machine Deleted" }; }
} );
export const disableMachine = withServerPromise(
async (machine_id: string): Promise<{ message: string }> => {
await db
.update(machinesTable)
.set({
disabled: true,
})
.where(eq(machinesTable.id, machine_id));
revalidatePath("/machines");
return { message: "Machine Disabled" };
}
);
export const enableMachine = withServerPromise(
async (machine_id: string): Promise<{ message: string }> => {
await db
.update(machinesTable)
.set({
disabled: false,
})
.where(eq(machinesTable.id, machine_id));
revalidatePath("/machines");
return { message: "Machine Enabled" };
}
);

View File

@ -10,4 +10,3 @@ export async function deleteWorkflow(workflow_id: string) {
await db.delete(workflowTable).where(eq(workflowTable.id, workflow_id)); await db.delete(workflowTable).where(eq(workflowTable.id, workflow_id));
revalidatePath("/"); revalidatePath("/");
} }

View File

@ -7,6 +7,6 @@ export async function wrapServerPromise<T>(result: Promise<T>) {
} }
export function withServerPromise<T extends (...args: any[]) => Promise<any>>( export function withServerPromise<T extends (...args: any[]) => Promise<any>>(
fn: T fn: T
): (...args: Parameters<T>) => Promise<ReturnType<T> | { error: string; }> { ): (...args: Parameters<T>) => Promise<ReturnType<T> | { error: string }> {
return (...args: Parameters<T>) => wrapServerPromise(fn(...args)); return (...args: Parameters<T>) => wrapServerPromise(fn(...args));
} }