214 lines
5.6 KiB
TypeScript
214 lines
5.6 KiB
TypeScript
/* Typed client for the kirche-einfach API (served via vite /api proxy). */
|
|
|
|
export interface FlowDefinition {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
status: string;
|
|
source_path: string | null;
|
|
continuation_flow_id: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface FlowVersion {
|
|
id: string;
|
|
flow_id: string;
|
|
version_no: number;
|
|
langflow_json: unknown;
|
|
lfx_flow_id: string | null;
|
|
deployed_at: string | null;
|
|
author: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface RunOutput {
|
|
result?: string;
|
|
component?: string;
|
|
type?: string;
|
|
error?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface Run {
|
|
id: string;
|
|
version_id: string;
|
|
tenant_id: string;
|
|
status: string;
|
|
input: unknown;
|
|
output: RunOutput | null;
|
|
session_id: string | null;
|
|
created_at: string;
|
|
finished_at: string | null;
|
|
}
|
|
|
|
export interface ApprovalTask {
|
|
id: string;
|
|
run_id: string;
|
|
version_id: string;
|
|
tenant_id: string;
|
|
title: string;
|
|
payload: unknown;
|
|
continuation_flow_id: string | null;
|
|
approver: string | null;
|
|
status: string;
|
|
decision_note: string;
|
|
created_at: string;
|
|
decided_at: string | null;
|
|
}
|
|
|
|
export interface Decision {
|
|
task: ApprovalTask;
|
|
continuation_run: Run | null;
|
|
}
|
|
|
|
export interface SyncReport {
|
|
added: string[];
|
|
updated: string[];
|
|
deleted: string[];
|
|
skipped: string[];
|
|
pulled: string[];
|
|
errors: Record<string, string>;
|
|
}
|
|
|
|
export interface Me {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
role: string;
|
|
tenant_id: string;
|
|
}
|
|
|
|
export interface AccessRequest {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
reason: string;
|
|
status: string;
|
|
created_at: string;
|
|
decided_at: string | null;
|
|
decided_by: string | null;
|
|
}
|
|
|
|
export interface InputSchema {
|
|
flow_id: string;
|
|
version_no: number;
|
|
json_schema: Record<string, unknown>;
|
|
ui_schema: Record<string, unknown>;
|
|
fallback: string | null;
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
constructor(status: number, message: string) {
|
|
super(message);
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
async function req<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const res = await fetch(path, {
|
|
// Session cookie (HttpOnly) must travel with every request.
|
|
credentials: "include",
|
|
headers: { "Content-Type": "application/json" },
|
|
...init,
|
|
});
|
|
if (!res.ok) {
|
|
let detail = res.statusText;
|
|
try {
|
|
const body: unknown = await res.json();
|
|
if (
|
|
typeof body === "object" &&
|
|
body !== null &&
|
|
"detail" in body &&
|
|
typeof (body as { detail: unknown }).detail === "string"
|
|
) {
|
|
detail = (body as { detail: string }).detail;
|
|
}
|
|
} catch {
|
|
/* keep statusText */
|
|
}
|
|
throw new ApiError(res.status, detail);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export const LANGFLOW_URL =
|
|
(import.meta.env.VITE_LANGFLOW_URL as string | undefined) ??
|
|
"http://localhost:7860";
|
|
|
|
export const api = {
|
|
listFlows: () => req<FlowDefinition[]>("/api/flows"),
|
|
listVersions: (flowId: string) =>
|
|
req<FlowVersion[]>(`/api/flows/${flowId}/versions`),
|
|
deployVersion: (flowId: string, versionNo: number) =>
|
|
req<FlowVersion>(`/api/flows/${flowId}/versions/${versionNo}/deploy`, {
|
|
method: "POST",
|
|
}),
|
|
runFlow: (flowId: string, input: unknown) =>
|
|
req<Run>(`/api/flows/${flowId}/run`, {
|
|
method: "POST",
|
|
// Tenant comes from the session server-side; no tenant box anymore.
|
|
body: JSON.stringify({ input }),
|
|
}),
|
|
getInputSchema: (flowId: string) =>
|
|
req<InputSchema>(`/api/flows/${flowId}/input-schema`),
|
|
listRuns: (flowId: string) => req<Run[]>(`/api/flows/${flowId}/runs`),
|
|
listApprovals: () => req<ApprovalTask[]>("/api/approvals"),
|
|
decide: (taskId: string, decision: "approve" | "reject", note: string) =>
|
|
req<Decision>(`/api/approvals/${taskId}/decision`, {
|
|
method: "POST",
|
|
// Identity comes from the session; the server records the user.
|
|
body: JSON.stringify({ decision, note }),
|
|
}),
|
|
importFlow: (langflowFlowId: string, name?: string) =>
|
|
req<{ definition: FlowDefinition; version: FlowVersion }>(
|
|
"/api/flows/import",
|
|
{
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
langflow_flow_id: langflowFlowId,
|
|
...(name ? { name } : {}),
|
|
}),
|
|
},
|
|
),
|
|
listLangflow: () =>
|
|
req<{ id: string; name: string }[]>("/api/flows/langflow/available"),
|
|
syncFlows: () =>
|
|
req<SyncReport>("/api/flows/sync", {
|
|
method: "POST",
|
|
}),
|
|
me: async (): Promise<Me | null> => {
|
|
try {
|
|
return await req<Me>("/api/auth/me");
|
|
} catch (e) {
|
|
if (e instanceof ApiError && e.status === 401) return null;
|
|
throw e;
|
|
}
|
|
},
|
|
consumeMagic: (token: string) =>
|
|
req<Me>("/api/auth/magic/consume", {
|
|
method: "POST",
|
|
body: JSON.stringify({ token }),
|
|
}),
|
|
requestMagicLink: (email: string) =>
|
|
req<{ message: string; email_sent: boolean }>("/api/auth/magic/request", {
|
|
method: "POST",
|
|
body: JSON.stringify({ email }),
|
|
}),
|
|
logout: () =>
|
|
req<{ message: string }>("/api/auth/logout", { method: "POST" }),
|
|
requestAccess: (name: string, email: string, reason: string) =>
|
|
req<AccessRequest>("/api/access/requests", {
|
|
method: "POST",
|
|
body: JSON.stringify({ name, email, reason }),
|
|
}),
|
|
listAccessRequests: (status?: string) =>
|
|
req<AccessRequest[]>(
|
|
`/api/access/requests${status ? `?status=${status}` : ""}`,
|
|
),
|
|
decideAccess: (id: string, approve: boolean) =>
|
|
req<{ request: AccessRequest; email_sent: boolean }>(
|
|
`/api/access/requests/${id}/${approve ? "approve" : "reject"}`,
|
|
{ method: "POST" },
|
|
),
|
|
};
|