diff --git a/web/bun.lockb b/web/bun.lockb index 907bdf8..6cd3e69 100755 Binary files a/web/bun.lockb and b/web/bun.lockb differ diff --git a/web/package.json b/web/package.json index 173a59b..918f4e6 100644 --- a/web/package.json +++ b/web/package.json @@ -21,6 +21,10 @@ "@clerk/nextjs": "^4.27.4", "@headlessui/react": "^1.7.17", "@headlessui/tailwindcss": "^0.2.0", + "@hono/node-server": "^1.4.0", + "@hono/swagger-ui": "^0.2.1", + "@hono/zod-openapi": "^0.9.5", + "@hono/zod-validator": "^0.1.11", "@hookform/resolvers": "^3.3.2", "@mdx-js/loader": "^3.0.0", "@mdx-js/react": "^3.0.0", @@ -46,6 +50,7 @@ "@tanstack/react-table": "^8.10.7", "@types/jsonwebtoken": "^9.0.5", "@types/react-highlight-words": "^0.16.7", + "@types/swagger-ui-react": "^4.18.3", "@types/uuid": "^9.0.7", "acorn": "^8.11.2", "class-variance-authority": "^0.7.0", @@ -58,6 +63,7 @@ "fast-glob": "^3.3.2", "flexsearch": "^0.7.31", "framer-motion": "^10.16.16", + "hono": "^3.12.0", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.294.0", "mdast-util-to-string": "^4.0.0", @@ -83,6 +89,7 @@ "shikiji": "^0.9.3", "simple-functional-loader": "^1.2.1", "sonner": "^1.2.4", + "swagger-ui-react": "^5.11.0", "swr": "^2.2.4", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7", diff --git a/web/src/app/(app)/api/[[...routes]]/route.ts b/web/src/app/(app)/api/[[...routes]]/route.ts new file mode 100644 index 0000000..d7dd327 --- /dev/null +++ b/web/src/app/(app)/api/[[...routes]]/route.ts @@ -0,0 +1,295 @@ +import { createRun } from "../../../../server/createRun"; +import { db } from "@/db/db"; +import { deploymentsTable, workflowRunsTable } from "@/db/schema"; +import { createSelectSchema } from "@/lib/drizzle-zod-hono"; +import { isKeyRevoked } from "@/server/curdApiKeys"; +import { getRunsData } from "@/server/getRunsOutput"; +import { parseJWT } from "@/server/parseJWT"; +import { replaceCDNUrl } from "@/server/replaceCDNUrl"; +import type { ResponseConfig } from "@asteasolutions/zod-to-openapi"; +import { z, createRoute } from "@hono/zod-openapi"; +import { OpenAPIHono } from "@hono/zod-openapi"; +import { eq } from "drizzle-orm"; +import { handle } from "hono/vercel"; + +export const dynamic = "force-dynamic"; + +export const app = new OpenAPIHono().basePath("/api"); + +declare module "hono" { + interface ContextVariableMap { + apiKeyTokenData: ReturnType; + } +} + +const authError = { + 401: { + content: { + "text/plain": { + schema: z.string().openapi({ + type: "string", + example: "Invalid or expired token", + }), + }, + }, + description: "Invalid or expired token", + }, +} satisfies { + [statusCode: string]: ResponseConfig; +}; + +app.use("/run", async (c, next) => { + const token = c.req.raw.headers.get("Authorization")?.split(" ")?.[1]; // Assuming token is sent as "Bearer your_token" + const userData = token ? parseJWT(token) : undefined; + if (!userData || token === undefined) { + return c.text("Invalid or expired token", 401); + } else { + const revokedKey = await isKeyRevoked(token); + if (revokedKey) return c.text("Revoked token", 401); + } + + c.set("apiKeyTokenData", userData); + + await next(); +}); + +// console.log(RunOutputZod.shape); + +const getOutputRoute = createRoute({ + method: "get", + path: "/run", + tags: ["workflows"], + summary: "Get workflow run output", + description: + "Call this to get a run's output, usually in conjunction with polling method", + request: { + query: z.object({ + run_id: z.string(), + }), + }, + responses: { + 200: { + content: { + "application/json": { + // https://github.com/asteasolutions/zod-to-openapi/issues/194 + schema: createSelectSchema(workflowRunsTable, { + workflow_inputs: (schema) => + schema.workflow_inputs.openapi({ + type: "object", + example: { + input_text: "some external text input", + input_image: "https://somestatic.png", + }, + }), + }), + }, + }, + description: "Retrieve the output", + }, + 400: { + content: { + "application/json": { + schema: z.object({ + code: z.number().openapi({ + type: "string", + example: 400, + }), + message: z.string().openapi({ + type: "string", + example: "Workflow not found", + }), + }), + }, + }, + description: "Workflow not found", + }, + 500: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: "Error getting output", + }, + ...authError, + }, +}); + +app.openapi(getOutputRoute, async (c) => { + const data = c.req.valid("query"); + const apiKeyTokenData = c.get("apiKeyTokenData")!; + + try { + const run = await getRunsData(apiKeyTokenData, data.run_id); + + if (!run) + return c.json( + { + code: 400, + message: "Workflow not found", + }, + 400 + ); + + // Fill in the CDN url + if (run?.status === "success" && run?.outputs?.length > 0) { + for (let i = 0; i < run.outputs.length; i++) { + const output = run.outputs[i]; + + if (output.data?.images !== undefined) { + for (let j = 0; j < output.data?.images.length; j++) { + const element = output.data?.images[j]; + element.url = replaceCDNUrl( + `${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${run.id}/${element.filename}` + ); + } + } else if (output.data?.files !== undefined) { + for (let j = 0; j < output.data?.files.length; j++) { + const element = output.data?.files[j]; + element.url = replaceCDNUrl( + `${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${run.id}/${element.filename}` + ); + } + } + } + } + + return c.json(run, 200); + } catch (error: any) { + return c.json( + { + error: error.message, + }, + { + status: 500, + } + ); + } +}); + +const createRunRoute = createRoute({ + method: "post", + path: "/run", + tags: ["workflows"], + summary: "Run a workflow via deployment_id", + request: { + body: { + content: { + "application/json": { + schema: z.object({ + deployment_id: z.string(), + inputs: z.record(z.string()).optional(), + }), + }, + }, + }, + // headers: z.object({ + // "Authorization": z. + // }) + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + run_id: z.string(), + }), + }, + }, + description: "Workflow queued", + }, + 500: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: "Error creating run", + }, + ...authError, + }, +}); + +app.openapi(createRunRoute, async (c) => { + const data = c.req.valid("json"); + const origin = new URL(c.req.url).origin; + const apiKeyTokenData = c.get("apiKeyTokenData")!; + + const { deployment_id, inputs } = data; + + try { + const deploymentData = await db.query.deploymentsTable.findFirst({ + where: eq(deploymentsTable.id, deployment_id), + with: { + machine: true, + version: { + with: { + workflow: { + columns: { + org_id: true, + user_id: true, + }, + }, + }, + }, + }, + }); + + if (!deploymentData) throw new Error("Deployment not found"); + + const run_id = await createRun({ + origin, + workflow_version_id: deploymentData.version, + machine_id: deploymentData.machine, + inputs, + isManualRun: false, + apiUser: apiKeyTokenData, + }); + + if ("error" in run_id) throw new Error(run_id.error); + + return c.json({ + run_id: "workflow_run_id" in run_id ? run_id.workflow_run_id : "", + }); + } catch (error: any) { + return c.json( + { + error: error.message, + }, + { + status: 500, + } + ); + } +}); + +// The OpenAPI documentation will be available at /doc +app.doc("/doc", { + openapi: "3.0.0", + servers: [{ url: "/api" }], + security: [{ bearerAuth: [] }], + info: { + version: "0.0.1", + title: "Comfy Deploy API", + description: + "Interact with Comfy Deploy programmatically to trigger run and retrieve output", + }, +}); + +app.openAPIRegistry.registerComponent("securitySchemes", "bearerAuth", { + type: "apiKey", + bearerFormat: "JWT", + in: "header", + name: "Authorization", + description: + "API token created in Comfy Deploy /api-keys", +}); + +const handler = handle(app); + +export const GET = handler; +export const POST = handler; diff --git a/web/src/app/(app)/api/run/route.ts b/web/src/app/(app)/api/run/route.ts deleted file mode 100644 index a613814..0000000 --- a/web/src/app/(app)/api/run/route.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { parseDataSafe } from "../../../../lib/parseDataSafe"; -import { createRun } from "../../../../server/createRun"; -import { db } from "@/db/db"; -import { deploymentsTable } from "@/db/schema"; -import { isKeyRevoked } from "@/server/curdApiKeys"; -import { getRunsData } from "@/server/getRunsOutput"; -import { parseJWT } from "@/server/parseJWT"; -import { replaceCDNUrl } from "@/server/replaceCDNUrl"; -import { eq } from "drizzle-orm"; -import { NextResponse } from "next/server"; -import { z } from "zod"; - -export const dynamic = "force-dynamic"; - -const Request = z.object({ - deployment_id: z.string(), - inputs: z.record(z.string()).optional(), -}); - -const Request2 = z.object({ - run_id: z.string(), -}); - -async function checkToken(request: Request) { - const token = request.headers.get("Authorization")?.split(" ")?.[1]; // Assuming token is sent as "Bearer your_token" - const userData = token ? parseJWT(token) : undefined; - if (!userData || token === undefined) { - return { - error: new NextResponse("Invalid or expired token", { - status: 401, - }), - }; - } else { - const revokedKey = await isKeyRevoked(token); - if (revokedKey) - return { - error: new NextResponse("Revoked token", { - status: 401, - }), - }; - } - - return { - data: userData, - }; -} - -export async function GET(request: Request) { - const apiKeyTokenData = await checkToken(request); - if (apiKeyTokenData.error) return apiKeyTokenData.error; - - const [data, error] = await parseDataSafe(Request2, request); - if (!data || error) return error; - - // return NextResponse.json( - // await db - // .select() - // .from(workflowTable) - // .innerJoin( - // workflowRunsTable, - // eq(workflowTable.id, workflowRunsTable.workflow_id) - // ) - // .where( - // and( - // eq(workflowTable.id, workflowRunsTable.workflow_id), - // apiKeyTokenData.data.org_id - // ? eq(workflowTable.org_id, apiKeyTokenData.data.org_id) - // : eq(workflowTable.user_id, apiKeyTokenData.data.user_id!) - // ) - // ), - // { - // status: 200, - // } - // ); - - const run = await getRunsData(apiKeyTokenData.data, data.run_id); - - if (!run) return new NextResponse("Run not found", { status: 404 }); - - if (run?.status === "success" && run?.outputs?.length > 0) { - for (let i = 0; i < run.outputs.length; i++) { - const output = run.outputs[i]; - - if (output.data?.images !== undefined) { - for (let j = 0; j < output.data?.images.length; j++) { - const element = output.data?.images[j]; - element.url = replaceCDNUrl( - `${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${run.id}/${element.filename}` - ); - } - } else if (output.data?.files !== undefined) { - for (let j = 0; j < output.data?.files.length; j++) { - const element = output.data?.files[j]; - element.url = replaceCDNUrl( - `${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${run.id}/${element.filename}` - ); - } - } - } - } - - return NextResponse.json(run, { - status: 200, - }); -} - -export async function POST(request: Request) { - const apiKeyTokenData = await checkToken(request); - if (apiKeyTokenData.error) return apiKeyTokenData.error; - - const [data, error] = await parseDataSafe(Request, request); - if (!data || error) return error; - - const origin = new URL(request.url).origin; - - const { deployment_id, inputs } = data; - - try { - const deploymentData = await db.query.deploymentsTable.findFirst({ - where: eq(deploymentsTable.id, deployment_id), - with: { - machine: true, - version: { - with: { - workflow: { - columns: { - org_id: true, - user_id: true, - }, - }, - }, - }, - }, - }); - - if (!deploymentData) throw new Error("Deployment not found"); - - const run_id = await createRun({ - origin, - workflow_version_id: deploymentData.version, - machine_id: deploymentData.machine, - inputs, - isManualRun: false, - apiUser: apiKeyTokenData.data, - }); - - if ("error" in run_id) throw new Error(run_id.error); - - return NextResponse.json( - { - run_id: "workflow_run_id" in run_id ? run_id.workflow_run_id : "", - }, - { - status: 200, - } - ); - } catch (error: any) { - return NextResponse.json( - { - error: error.message, - }, - { - status: 500, - } - ); - } -} diff --git a/web/src/app/(docs)/docs/endpoints/page.mdx b/web/src/app/(docs)/docs/endpoints/page.mdx new file mode 100644 index 0000000..b88ebff --- /dev/null +++ b/web/src/app/(docs)/docs/endpoints/page.mdx @@ -0,0 +1,91 @@ +export const metadata = { + title: 'Workflow API', + description: + 'Get started with API integration to run any deploy ComfyUI workflow', +} + +{/* # Workflow API */} + +{/* Get started with API integration to run any deploy ComfyUI workflow */} + + + +{/* ## Trigger a run {{ tag: 'POST', label: '/api/run' }} + + + + + Trigger a run with a deployment id + + ### Optional attributes + + + + Limit to attachments from a given conversation. + + + Limit the number of attachments returned. + + + + + + + + + ```bash {{ title: 'cURL' }} + curl -G https://api.protocol.chat/v1/attachments \ + -H "Authorization: Bearer {token}" \ + -d conversation_id="xgQQXg3hrtjh7AvZ" \ + -d limit=10 + ``` + + ```js + import ApiClient from '@example/protocol-api' + + const client = new ApiClient(token) + + await client.attachments.list() + ``` + + ```python + from protocol_api import ApiClient + + client = ApiClient(token) + + client.attachments.list() + ``` + + ```php + $client = new \Protocol\ApiClient($token); + + $client->attachments->list(); + ``` + + + + ```json {{ title: 'Response' }} + { + "has_more": false, + "data": [ + { + "id": "Nc6yKKMpcxiiFxp6", + "message_id": "LoPsJaMcPBuFNjg1", + "filename": "Invoice_room_service__Plaza_Hotel.pdf", + "file_url": "https://assets.protocol.chat/attachments/Invoice_room_service__Plaza_Hotel.pdf", + "file_type": "application/pdf", + "file_size": 21352, + "created_at": 692233200 + }, + { + "id": "hSIhXBhNe8X1d8Et" + // ... + } + ] + } + ``` + + + + +--- */} \ No newline at end of file diff --git a/web/src/components/docs/Navigation.tsx b/web/src/components/docs/Navigation.tsx index 29c62fa..ecef091 100644 --- a/web/src/components/docs/Navigation.tsx +++ b/web/src/components/docs/Navigation.tsx @@ -238,6 +238,10 @@ export const navigation: Array = [ { title: "Installation", href: "/docs/install" }, ], }, + { + title: "API", + links: [{ title: "Endpoints", href: "/docs/endpoints" }], + }, ]; export function Navigation(props: React.ComponentPropsWithoutRef<"nav">) { diff --git a/web/src/components/docs/SwaggerUIClient.css b/web/src/components/docs/SwaggerUIClient.css new file mode 100644 index 0000000..2a0c716 --- /dev/null +++ b/web/src/components/docs/SwaggerUIClient.css @@ -0,0 +1,3 @@ +.swagger-ui .info { + margin: 0 !important; +} \ No newline at end of file diff --git a/web/src/components/docs/SwaggerUIClient.tsx b/web/src/components/docs/SwaggerUIClient.tsx new file mode 100644 index 0000000..ceb7854 --- /dev/null +++ b/web/src/components/docs/SwaggerUIClient.tsx @@ -0,0 +1,39 @@ +"use client"; + +import "./SwaggerUIClient.css"; +import type { ComponentProps } from "react"; +import React from "react"; +import _SwaggerUI from "swagger-ui-react"; +import "swagger-ui-react/swagger-ui.css"; + +// Create the layout component +class AugmentingLayout extends React.Component { + render() { + const { getComponent } = this.props; + const BaseLayout = getComponent("BaseLayout", true); + return ( +
+ +
+ ); + } +} + +const AugmentingLayoutPlugin = () => { + return { + components: { + AugmentingLayout: AugmentingLayout, + }, + }; +}; + +export default function SwaggerUI(props: ComponentProps) { + return ( + <_SwaggerUI + {...props} + persistAuthorization={true} + plugins={[AugmentingLayoutPlugin]} + layout="AugmentingLayout" + /> + ); +} diff --git a/web/src/components/docs/mdx.tsx b/web/src/components/docs/mdx.tsx index 7f4a2ae..1be9302 100644 --- a/web/src/components/docs/mdx.tsx +++ b/web/src/components/docs/mdx.tsx @@ -2,9 +2,16 @@ import { Feedback } from "@/components/docs/Feedback"; import { Heading } from "@/components/docs/Heading"; import { Prose } from "@/components/docs/Prose"; import { cn } from "@/lib/utils"; +// import { SwaggerUI } from "@hono/swagger-ui"; import clsx from "clsx"; +import dynamic from "next/dynamic"; +// import _SwaggerUI from "swagger-ui-react"; import Link from "next/link"; +export const SwaggerUI = dynamic(() => import("./SwaggerUIClient"), { + ssr: false, +}); + export const a = Link; export { Button } from "@/components/docs/Button"; export { CodeGroup, Code as code, Pre as pre } from "@/components/docs/Code"; diff --git a/web/src/lib/drizzle-zod-hono.ts b/web/src/lib/drizzle-zod-hono.ts new file mode 100644 index 0000000..7174548 --- /dev/null +++ b/web/src/lib/drizzle-zod-hono.ts @@ -0,0 +1,309 @@ +import { z } from "@hono/zod-openapi"; +import { + type Assume, + type Column, + type DrizzleTypeError, + type Equal, + getTableColumns, + is, + type Simplify, + type Table, +} from "drizzle-orm"; +import { + MySqlChar, + MySqlVarBinary, + MySqlVarChar, +} from "drizzle-orm/mysql-core"; +import { type PgArray, PgChar, PgUUID, PgVarchar } from "drizzle-orm/pg-core"; +import { SQLiteText } from "drizzle-orm/sqlite-core"; + +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; +export const jsonSchema: z.ZodType = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +type MapInsertColumnToZod< + TColumn extends Column, + TType extends z.ZodTypeAny +> = TColumn["_"]["notNull"] extends false + ? z.ZodOptional> + : TColumn["_"]["hasDefault"] extends true + ? z.ZodOptional + : TType; + +type MapSelectColumnToZod< + TColumn extends Column, + TType extends z.ZodTypeAny +> = TColumn["_"]["notNull"] extends false ? z.ZodNullable : TType; + +type MapColumnToZod< + TColumn extends Column, + TType extends z.ZodTypeAny, + TMode extends "insert" | "select" +> = TMode extends "insert" + ? MapInsertColumnToZod + : MapSelectColumnToZod; + +type MaybeOptional< + TColumn extends Column, + TType extends z.ZodTypeAny, + TMode extends "insert" | "select", + TNoOptional extends boolean +> = TNoOptional extends true ? TType : MapColumnToZod; + +type GetZodType = + TColumn["_"]["dataType"] extends infer TDataType + ? TDataType extends "custom" + ? z.ZodAny + : TDataType extends "json" + ? z.ZodType + : TColumn extends { enumValues: [string, ...string[]] } + ? Equal extends true + ? z.ZodString + : z.ZodEnum + : TDataType extends "array" + ? z.ZodArray< + GetZodType["baseColumn"]> + > + : TDataType extends "bigint" + ? z.ZodBigInt + : TDataType extends "number" + ? z.ZodNumber + : TDataType extends "string" + ? z.ZodString + : TDataType extends "boolean" + ? z.ZodBoolean + : TDataType extends "date" + ? // ? z.ZodDate + z.ZodString + : z.ZodAny + : never; + +type ValueOrUpdater = T | ((arg: TUpdaterArg) => T); + +type UnwrapValueOrUpdater = T extends ValueOrUpdater + ? U + : never; + +export type Refine = { + [K in keyof TTable["_"]["columns"]]?: ValueOrUpdater< + z.ZodTypeAny, + TMode extends "select" + ? BuildSelectSchema + : BuildInsertSchema + >; +}; + +export type BuildInsertSchema< + TTable extends Table, + TRefine extends Refine | {}, + TNoOptional extends boolean = false +> = TTable["_"]["columns"] extends infer TColumns extends Record< + string, + Column +> + ? { + [K in keyof TColumns & string]: MaybeOptional< + TColumns[K], + K extends keyof TRefine + ? Assume, z.ZodTypeAny> + : GetZodType, + "insert", + TNoOptional + >; + } + : never; + +export type BuildSelectSchema< + TTable extends Table, + TRefine extends Refine, + TNoOptional extends boolean = false +> = Simplify<{ + [K in keyof TTable["_"]["columns"]]: MaybeOptional< + TTable["_"]["columns"][K], + K extends keyof TRefine + ? Assume, z.ZodTypeAny> + : GetZodType, + "select", + TNoOptional + >; +}>; + +export function createInsertSchema< + TTable extends Table, + TRefine extends Refine = Refine +>( + table: TTable, + /** + * @param refine Refine schema fields + */ + refine?: { + [K in keyof TRefine]: K extends keyof TTable["_"]["columns"] + ? TRefine[K] + : DrizzleTypeError<`Column '${K & + string}' does not exist in table '${TTable["_"]["name"]}'`>; + } +): z.ZodObject< + BuildInsertSchema< + TTable, + Equal> extends true ? {} : TRefine + > +> { + const columns = getTableColumns(table); + const columnEntries = Object.entries(columns); + + let schemaEntries = Object.fromEntries( + columnEntries.map(([name, column]) => { + return [name, mapColumnToSchema(column)]; + }) + ); + + if (refine) { + schemaEntries = Object.assign( + schemaEntries, + Object.fromEntries( + Object.entries(refine).map(([name, refineColumn]) => { + return [ + name, + typeof refineColumn === "function" + ? refineColumn( + schemaEntries as BuildInsertSchema + ) + : refineColumn, + ]; + }) + ) + ); + } + + for (const [name, column] of columnEntries) { + if (!column.notNull) { + schemaEntries[name] = schemaEntries[name]!.nullable().optional(); + } else if (column.hasDefault) { + schemaEntries[name] = schemaEntries[name]!.optional(); + } + } + + return z.object(schemaEntries) as any; +} + +export function createSelectSchema< + TTable extends Table, + TRefine extends Refine = Refine +>( + table: TTable, + /** + * @param refine Refine schema fields + */ + refine?: { + [K in keyof TRefine]: K extends keyof TTable["_"]["columns"] + ? TRefine[K] + : DrizzleTypeError<`Column '${K & + string}' does not exist in table '${TTable["_"]["name"]}'`>; + } +): z.ZodObject< + BuildSelectSchema< + TTable, + Equal> extends true ? {} : TRefine + > +> { + const columns = getTableColumns(table); + const columnEntries = Object.entries(columns); + + let schemaEntries = Object.fromEntries( + columnEntries.map(([name, column]) => { + return [name, mapColumnToSchema(column)]; + }) + ); + + if (refine) { + schemaEntries = Object.assign( + schemaEntries, + Object.fromEntries( + Object.entries(refine).map(([name, refineColumn]) => { + return [ + name, + typeof refineColumn === "function" + ? refineColumn( + schemaEntries as BuildSelectSchema + ) + : refineColumn, + ]; + }) + ) + ); + } + + for (const [name, column] of columnEntries) { + if (!column.notNull) { + schemaEntries[name] = schemaEntries[name]!.nullable(); + } + } + + return z.object(schemaEntries) as any; +} + +function isWithEnum( + column: Column +): column is typeof column & { enumValues: [string, ...string[]] } { + return ( + "enumValues" in column && + Array.isArray(column.enumValues) && + column.enumValues.length > 0 + ); +} + +function mapColumnToSchema(column: Column): z.ZodTypeAny { + let type: z.ZodTypeAny | undefined; + + if (isWithEnum(column)) { + type = column.enumValues.length ? z.enum(column.enumValues) : z.string(); + } + + if (!type) { + if (is(column, PgUUID)) { + type = z.string().uuid(); + } else if (column.dataType === "custom") { + type = z.any(); + } else if (column.dataType === "json") { + type = jsonSchema; + } else if (column.dataType === "array") { + type = z.array( + mapColumnToSchema((column as PgArray).baseColumn) + ); + } else if (column.dataType === "number") { + type = z.number(); + } else if (column.dataType === "bigint") { + type = z.bigint(); + } else if (column.dataType === "boolean") { + type = z.boolean(); + } else if (column.dataType === "date") { + // type = z.date(); + type = z.string(); + } else if (column.dataType === "string") { + let sType = z.string(); + + if ( + (is(column, PgChar) || + is(column, PgVarchar) || + is(column, MySqlVarChar) || + is(column, MySqlVarBinary) || + is(column, MySqlChar) || + is(column, SQLiteText)) && + typeof column.length === "number" + ) { + sType = sType.max(column.length); + } + + type = sType; + } + } + + if (!type) { + type = z.any(); + } + + return type; +} diff --git a/web/src/server/getRunsOutput.tsx b/web/src/server/getRunsOutput.tsx index 8716fee..52d137c 100644 --- a/web/src/server/getRunsOutput.tsx +++ b/web/src/server/getRunsOutput.tsx @@ -20,29 +20,7 @@ export async function getRunsOutput(run_id: string) { export async function getRunsData(user: APIKeyUserType, run_id: string) { const data = await db.query.workflowRunsTable.findFirst({ - where: and( - eq(workflowRunsTable.id, run_id) - // inArray( - // workflowRunsTable.workflow_id, - // db - // .select({ - // id: workflowTable.id, - // }) - // .from(workflowTable) - // .innerJoin( - // workflowRunsTable, - // eq(workflowTable.id, workflowRunsTable.workflow_id) - // ) - // .where( - // and( - // eq(workflowTable.id, workflowRunsTable.workflow_id), - // user.org_id - // ? eq(workflowTable.org_id, user.org_id) - // : eq(workflowTable.user_id, user.user_id!) - // ) - // ) - // ) - ), + where: and(eq(workflowRunsTable.id, run_id)), with: { workflow: { columns: {