feat: add interactive api docs, rewrite run endpoint with hono and zod validator
This commit is contained in:
parent
e921d4c320
commit
7d36f8af88
BIN
web/bun.lockb
BIN
web/bun.lockb
Binary file not shown.
@ -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",
|
||||||
|
295
web/src/app/(app)/api/[[...routes]]/route.ts
Normal file
295
web/src/app/(app)/api/[[...routes]]/route.ts
Normal 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;
|
@ -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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
91
web/src/app/(docs)/docs/endpoints/page.mdx
Normal file
91
web/src/app/(docs)/docs/endpoints/page.mdx
Normal 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>
|
||||||
|
|
||||||
|
--- */}
|
@ -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">) {
|
||||||
|
3
web/src/components/docs/SwaggerUIClient.css
Normal file
3
web/src/components/docs/SwaggerUIClient.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.swagger-ui .info {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
39
web/src/components/docs/SwaggerUIClient.tsx
Normal file
39
web/src/components/docs/SwaggerUIClient.tsx
Normal 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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -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";
|
||||||
|
309
web/src/lib/drizzle-zod-hono.ts
Normal file
309
web/src/lib/drizzle-zod-hono.ts
Normal 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;
|
||||||
|
}
|
@ -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: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user