feat: add interactive api docs, rewrite run endpoint with hono and zod validator

This commit is contained in:
BennyKok 2024-01-09 14:00:16 +08:00
parent e921d4c320
commit 7d36f8af88
11 changed files with 756 additions and 190 deletions

Binary file not shown.

View File

@ -21,6 +21,10 @@
"@clerk/nextjs": "^4.27.4", "@clerk/nextjs": "^4.27.4",
"@headlessui/react": "^1.7.17", "@headlessui/react": "^1.7.17",
"@headlessui/tailwindcss": "^0.2.0", "@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", "@hookform/resolvers": "^3.3.2",
"@mdx-js/loader": "^3.0.0", "@mdx-js/loader": "^3.0.0",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
@ -46,6 +50,7 @@
"@tanstack/react-table": "^8.10.7", "@tanstack/react-table": "^8.10.7",
"@types/jsonwebtoken": "^9.0.5", "@types/jsonwebtoken": "^9.0.5",
"@types/react-highlight-words": "^0.16.7", "@types/react-highlight-words": "^0.16.7",
"@types/swagger-ui-react": "^4.18.3",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"acorn": "^8.11.2", "acorn": "^8.11.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
@ -58,6 +63,7 @@
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"flexsearch": "^0.7.31", "flexsearch": "^0.7.31",
"framer-motion": "^10.16.16", "framer-motion": "^10.16.16",
"hono": "^3.12.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-react": "^0.294.0", "lucide-react": "^0.294.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
@ -83,6 +89,7 @@
"shikiji": "^0.9.3", "shikiji": "^0.9.3",
"simple-functional-loader": "^1.2.1", "simple-functional-loader": "^1.2.1",
"sonner": "^1.2.4", "sonner": "^1.2.4",
"swagger-ui-react": "^5.11.0",
"swr": "^2.2.4", "swr": "^2.2.4",
"tailwind-merge": "^2.1.0", "tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

View File

@ -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<typeof parseJWT>;
}
}
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 <a href='/api-keys' target='_blank' style='text-decoration: underline;'>/api-keys</a>",
});
const handler = handle(app);
export const GET = handler;
export const POST = handler;

View File

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

View File

@ -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 */}
<SwaggerUI url={"/api/doc"}></SwaggerUI>
{/* ## Trigger a run {{ tag: 'POST', label: '/api/run' }}
<Row>
<Col>
Trigger a run with a deployment id
### Optional attributes
<Properties>
<Property name="conversation_id" type="string">
Limit to attachments from a given conversation.
</Property>
<Property name="limit" type="integer">
Limit the number of attachments returned.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/v1/attachments">
```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();
```
</CodeGroup>
```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"
// ...
}
]
}
```
</Col>
</Row>
--- */}

View File

@ -238,6 +238,10 @@ export const navigation: Array<NavGroup> = [
{ title: "Installation", href: "/docs/install" }, { title: "Installation", href: "/docs/install" },
], ],
}, },
{
title: "API",
links: [{ title: "Endpoints", href: "/docs/endpoints" }],
},
]; ];
export function Navigation(props: React.ComponentPropsWithoutRef<"nav">) { export function Navigation(props: React.ComponentPropsWithoutRef<"nav">) {

View File

@ -0,0 +1,3 @@
.swagger-ui .info {
margin: 0 !important;
}

View File

@ -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 (
<div className="not-prose">
<BaseLayout />
</div>
);
}
}
const AugmentingLayoutPlugin = () => {
return {
components: {
AugmentingLayout: AugmentingLayout,
},
};
};
export default function SwaggerUI(props: ComponentProps<typeof _SwaggerUI>) {
return (
<_SwaggerUI
{...props}
persistAuthorization={true}
plugins={[AugmentingLayoutPlugin]}
layout="AugmentingLayout"
/>
);
}

View File

@ -2,9 +2,16 @@ import { Feedback } from "@/components/docs/Feedback";
import { Heading } from "@/components/docs/Heading"; import { Heading } from "@/components/docs/Heading";
import { Prose } from "@/components/docs/Prose"; import { Prose } from "@/components/docs/Prose";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
// import { SwaggerUI } from "@hono/swagger-ui";
import clsx from "clsx"; import clsx from "clsx";
import dynamic from "next/dynamic";
// import _SwaggerUI from "swagger-ui-react";
import Link from "next/link"; import Link from "next/link";
export const SwaggerUI = dynamic(() => import("./SwaggerUIClient"), {
ssr: false,
});
export const a = Link; export const a = Link;
export { Button } from "@/components/docs/Button"; export { Button } from "@/components/docs/Button";
export { CodeGroup, Code as code, Pre as pre } from "@/components/docs/Code"; export { CodeGroup, Code as code, Pre as pre } from "@/components/docs/Code";

View File

@ -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<typeof literalSchema>;
type Json = Literal | { [key: string]: Json } | Json[];
export const jsonSchema: z.ZodType<Json> = 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<z.ZodNullable<TType>>
: TColumn["_"]["hasDefault"] extends true
? z.ZodOptional<TType>
: TType;
type MapSelectColumnToZod<
TColumn extends Column,
TType extends z.ZodTypeAny
> = TColumn["_"]["notNull"] extends false ? z.ZodNullable<TType> : TType;
type MapColumnToZod<
TColumn extends Column,
TType extends z.ZodTypeAny,
TMode extends "insert" | "select"
> = TMode extends "insert"
? MapInsertColumnToZod<TColumn, TType>
: MapSelectColumnToZod<TColumn, TType>;
type MaybeOptional<
TColumn extends Column,
TType extends z.ZodTypeAny,
TMode extends "insert" | "select",
TNoOptional extends boolean
> = TNoOptional extends true ? TType : MapColumnToZod<TColumn, TType, TMode>;
type GetZodType<TColumn extends Column> =
TColumn["_"]["dataType"] extends infer TDataType
? TDataType extends "custom"
? z.ZodAny
: TDataType extends "json"
? z.ZodType<Json>
: TColumn extends { enumValues: [string, ...string[]] }
? Equal<TColumn["enumValues"], [string, ...string[]]> extends true
? z.ZodString
: z.ZodEnum<TColumn["enumValues"]>
: TDataType extends "array"
? z.ZodArray<
GetZodType<Assume<TColumn["_"], { baseColumn: Column }>["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, TUpdaterArg> = T | ((arg: TUpdaterArg) => T);
type UnwrapValueOrUpdater<T> = T extends ValueOrUpdater<infer U, any>
? U
: never;
export type Refine<TTable extends Table, TMode extends "select" | "insert"> = {
[K in keyof TTable["_"]["columns"]]?: ValueOrUpdater<
z.ZodTypeAny,
TMode extends "select"
? BuildSelectSchema<TTable, {}, true>
: BuildInsertSchema<TTable, {}, true>
>;
};
export type BuildInsertSchema<
TTable extends Table,
TRefine extends Refine<TTable, "insert"> | {},
TNoOptional extends boolean = false
> = TTable["_"]["columns"] extends infer TColumns extends Record<
string,
Column<any>
>
? {
[K in keyof TColumns & string]: MaybeOptional<
TColumns[K],
K extends keyof TRefine
? Assume<UnwrapValueOrUpdater<TRefine[K]>, z.ZodTypeAny>
: GetZodType<TColumns[K]>,
"insert",
TNoOptional
>;
}
: never;
export type BuildSelectSchema<
TTable extends Table,
TRefine extends Refine<TTable, "select">,
TNoOptional extends boolean = false
> = Simplify<{
[K in keyof TTable["_"]["columns"]]: MaybeOptional<
TTable["_"]["columns"][K],
K extends keyof TRefine
? Assume<UnwrapValueOrUpdater<TRefine[K]>, z.ZodTypeAny>
: GetZodType<TTable["_"]["columns"][K]>,
"select",
TNoOptional
>;
}>;
export function createInsertSchema<
TTable extends Table,
TRefine extends Refine<TTable, "insert"> = Refine<TTable, "insert">
>(
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<TRefine, Refine<TTable, "insert">> 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<TTable, {}, true>
)
: 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<TTable, "select"> = Refine<TTable, "select">
>(
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<TRefine, Refine<TTable, "select">> 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<TTable, {}, true>
)
: 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<any, any>).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;
}

View File

@ -20,29 +20,7 @@ export async function getRunsOutput(run_id: string) {
export async function getRunsData(user: APIKeyUserType, run_id: string) { export async function getRunsData(user: APIKeyUserType, run_id: string) {
const data = await db.query.workflowRunsTable.findFirst({ const data = await db.query.workflowRunsTable.findFirst({
where: and( where: and(eq(workflowRunsTable.id, run_id)),
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!)
// )
// )
// )
),
with: { with: {
workflow: { workflow: {
columns: { columns: {