@nodetool-ai/websocket 0.7.0-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent-runtime.d.ts +47 -0
- package/dist/agent/agent-runtime.d.ts.map +1 -0
- package/dist/agent/agent-runtime.js +577 -0
- package/dist/agent/agent-runtime.js.map +1 -0
- package/dist/agent/codex-agent.d.ts +41 -0
- package/dist/agent/codex-agent.d.ts.map +1 -0
- package/dist/agent/codex-agent.js +334 -0
- package/dist/agent/codex-agent.js.map +1 -0
- package/dist/agent/index.d.ts +6 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +4 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/llm-agent.d.ts +51 -0
- package/dist/agent/llm-agent.d.ts.map +1 -0
- package/dist/agent/llm-agent.js +526 -0
- package/dist/agent/llm-agent.js.map +1 -0
- package/dist/agent/mcp-tool-server.d.ts +33 -0
- package/dist/agent/mcp-tool-server.d.ts.map +1 -0
- package/dist/agent/mcp-tool-server.js +292 -0
- package/dist/agent/mcp-tool-server.js.map +1 -0
- package/dist/agent/opencode-agent.d.ts +32 -0
- package/dist/agent/opencode-agent.d.ts.map +1 -0
- package/dist/agent/opencode-agent.js +332 -0
- package/dist/agent/opencode-agent.js.map +1 -0
- package/dist/agent/pi-agent.d.ts +42 -0
- package/dist/agent/pi-agent.d.ts.map +1 -0
- package/dist/agent/pi-agent.js +429 -0
- package/dist/agent/pi-agent.js.map +1 -0
- package/dist/agent/sdk-provider.d.ts +46 -0
- package/dist/agent/sdk-provider.d.ts.map +1 -0
- package/dist/agent/sdk-provider.js +50 -0
- package/dist/agent/sdk-provider.js.map +1 -0
- package/dist/agent/socket-route.d.ts +11 -0
- package/dist/agent/socket-route.d.ts.map +1 -0
- package/dist/agent/socket-route.js +262 -0
- package/dist/agent/socket-route.js.map +1 -0
- package/dist/agent/transport.d.ts +39 -0
- package/dist/agent/transport.d.ts.map +1 -0
- package/dist/agent/transport.js +17 -0
- package/dist/agent/transport.js.map +1 -0
- package/dist/agent/types.d.ts +11 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +10 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/collection-api.d.ts +14 -0
- package/dist/collection-api.d.ts.map +1 -0
- package/dist/collection-api.js +75 -0
- package/dist/collection-api.js.map +1 -0
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +2 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/file-api.d.ts +12 -0
- package/dist/file-api.d.ts.map +1 -0
- package/dist/file-api.js +76 -0
- package/dist/file-api.js.map +1 -0
- package/dist/http-api.d.ts +94 -0
- package/dist/http-api.d.ts.map +1 -0
- package/dist/http-api.js +1472 -0
- package/dist/http-api.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/asset-paths.d.ts +10 -0
- package/dist/lib/asset-paths.d.ts.map +1 -0
- package/dist/lib/asset-paths.js +45 -0
- package/dist/lib/asset-paths.js.map +1 -0
- package/dist/lib/bridge.d.ts +8 -0
- package/dist/lib/bridge.d.ts.map +1 -0
- package/dist/lib/bridge.js +70 -0
- package/dist/lib/bridge.js.map +1 -0
- package/dist/mcp-server.d.ts +32 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +842 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/models-api.d.ts +12 -0
- package/dist/models-api.d.ts.map +1 -0
- package/dist/models-api.js +730 -0
- package/dist/models-api.js.map +1 -0
- package/dist/oauth-api.d.ts +28 -0
- package/dist/oauth-api.d.ts.map +1 -0
- package/dist/oauth-api.js +681 -0
- package/dist/oauth-api.js.map +1 -0
- package/dist/openai-api.d.ts +63 -0
- package/dist/openai-api.d.ts.map +1 -0
- package/dist/openai-api.js +359 -0
- package/dist/openai-api.js.map +1 -0
- package/dist/plugins/websocket.d.ts +14 -0
- package/dist/plugins/websocket.d.ts.map +1 -0
- package/dist/plugins/websocket.js +431 -0
- package/dist/plugins/websocket.js.map +1 -0
- package/dist/resolve-media-urls.d.ts +19 -0
- package/dist/resolve-media-urls.d.ts.map +1 -0
- package/dist/resolve-media-urls.js +109 -0
- package/dist/resolve-media-urls.js.map +1 -0
- package/dist/routes/assets.d.ts +22 -0
- package/dist/routes/assets.d.ts.map +1 -0
- package/dist/routes/assets.js +168 -0
- package/dist/routes/assets.js.map +1 -0
- package/dist/routes/collections.d.ts +16 -0
- package/dist/routes/collections.d.ts.map +1 -0
- package/dist/routes/collections.js +32 -0
- package/dist/routes/collections.js.map +1 -0
- package/dist/routes/files.d.ts +12 -0
- package/dist/routes/files.d.ts.map +1 -0
- package/dist/routes/files.js +11 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/health.d.ts +4 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +41 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/jobs.d.ts +16 -0
- package/dist/routes/jobs.d.ts.map +1 -0
- package/dist/routes/jobs.js +25 -0
- package/dist/routes/jobs.js.map +1 -0
- package/dist/routes/nodes.d.ts +8 -0
- package/dist/routes/nodes.d.ts.map +1 -0
- package/dist/routes/nodes.js +124 -0
- package/dist/routes/nodes.js.map +1 -0
- package/dist/routes/oauth.d.ts +8 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +19 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/openai.d.ts +8 -0
- package/dist/routes/openai.d.ts.map +1 -0
- package/dist/routes/openai.js +20 -0
- package/dist/routes/openai.js.map +1 -0
- package/dist/routes/storage.d.ts +12 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +13 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/workflows.d.ts +18 -0
- package/dist/routes/workflows.d.ts.map +1 -0
- package/dist/routes/workflows.js +25 -0
- package/dist/routes/workflows.js.map +1 -0
- package/dist/routes/workspace.d.ts +15 -0
- package/dist/routes/workspace.d.ts.map +1 -0
- package/dist/routes/workspace.js +24 -0
- package/dist/routes/workspace.js.map +1 -0
- package/dist/screenshot-server.d.ts +21 -0
- package/dist/screenshot-server.d.ts.map +1 -0
- package/dist/screenshot-server.js +222 -0
- package/dist/screenshot-server.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +783 -0
- package/dist/server.js.map +1 -0
- package/dist/settings-registry.d.ts +29 -0
- package/dist/settings-registry.d.ts.map +1 -0
- package/dist/settings-registry.js +117 -0
- package/dist/settings-registry.js.map +1 -0
- package/dist/storage-api.d.ts +5 -0
- package/dist/storage-api.d.ts.map +1 -0
- package/dist/storage-api.js +221 -0
- package/dist/storage-api.js.map +1 -0
- package/dist/test-ui-server.d.ts +19 -0
- package/dist/test-ui-server.d.ts.map +1 -0
- package/dist/test-ui-server.js +1256 -0
- package/dist/test-ui-server.js.map +1 -0
- package/dist/trpc/context.d.ts +21 -0
- package/dist/trpc/context.d.ts.map +1 -0
- package/dist/trpc/context.js +10 -0
- package/dist/trpc/context.js.map +1 -0
- package/dist/trpc/error-formatter.d.ts +18 -0
- package/dist/trpc/error-formatter.d.ts.map +1 -0
- package/dist/trpc/error-formatter.js +41 -0
- package/dist/trpc/error-formatter.js.map +1 -0
- package/dist/trpc/index.d.ts +18 -0
- package/dist/trpc/index.d.ts.map +1 -0
- package/dist/trpc/index.js +13 -0
- package/dist/trpc/index.js.map +1 -0
- package/dist/trpc/middleware.d.ts +8 -0
- package/dist/trpc/middleware.d.ts.map +1 -0
- package/dist/trpc/middleware.js +12 -0
- package/dist/trpc/middleware.js.map +1 -0
- package/dist/trpc/router.d.ts +2543 -0
- package/dist/trpc/router.d.ts.map +1 -0
- package/dist/trpc/router.js +43 -0
- package/dist/trpc/router.js.map +1 -0
- package/dist/trpc/routers/assets.d.ts +229 -0
- package/dist/trpc/routers/assets.d.ts.map +1 -0
- package/dist/trpc/routers/assets.js +263 -0
- package/dist/trpc/routers/assets.js.map +1 -0
- package/dist/trpc/routers/collections.d.ts +88 -0
- package/dist/trpc/routers/collections.d.ts.map +1 -0
- package/dist/trpc/routers/collections.js +166 -0
- package/dist/trpc/routers/collections.js.map +1 -0
- package/dist/trpc/routers/costs.d.ts +129 -0
- package/dist/trpc/routers/costs.d.ts.map +1 -0
- package/dist/trpc/routers/costs.js +95 -0
- package/dist/trpc/routers/costs.js.map +1 -0
- package/dist/trpc/routers/files.d.ts +34 -0
- package/dist/trpc/routers/files.d.ts.map +1 -0
- package/dist/trpc/routers/files.js +92 -0
- package/dist/trpc/routers/files.js.map +1 -0
- package/dist/trpc/routers/jobs.d.ts +92 -0
- package/dist/trpc/routers/jobs.d.ts.map +1 -0
- package/dist/trpc/routers/jobs.js +100 -0
- package/dist/trpc/routers/jobs.js.map +1 -0
- package/dist/trpc/routers/mcp-config.d.ts +69 -0
- package/dist/trpc/routers/mcp-config.d.ts.map +1 -0
- package/dist/trpc/routers/mcp-config.js +290 -0
- package/dist/trpc/routers/mcp-config.js.map +1 -0
- package/dist/trpc/routers/messages.d.ts +121 -0
- package/dist/trpc/routers/messages.d.ts.map +1 -0
- package/dist/trpc/routers/messages.js +113 -0
- package/dist/trpc/routers/messages.js.map +1 -0
- package/dist/trpc/routers/models.d.ts +748 -0
- package/dist/trpc/routers/models.d.ts.map +1 -0
- package/dist/trpc/routers/models.js +1107 -0
- package/dist/trpc/routers/models.js.map +1 -0
- package/dist/trpc/routers/nodes.d.ts +48 -0
- package/dist/trpc/routers/nodes.d.ts.map +1 -0
- package/dist/trpc/routers/nodes.js +57 -0
- package/dist/trpc/routers/nodes.js.map +1 -0
- package/dist/trpc/routers/sandboxes.d.ts +64 -0
- package/dist/trpc/routers/sandboxes.d.ts.map +1 -0
- package/dist/trpc/routers/sandboxes.js +327 -0
- package/dist/trpc/routers/sandboxes.js.map +1 -0
- package/dist/trpc/routers/settings.d.ts +113 -0
- package/dist/trpc/routers/settings.d.ts.map +1 -0
- package/dist/trpc/routers/settings.js +193 -0
- package/dist/trpc/routers/settings.js.map +1 -0
- package/dist/trpc/routers/skills.d.ts +41 -0
- package/dist/trpc/routers/skills.d.ts.map +1 -0
- package/dist/trpc/routers/skills.js +194 -0
- package/dist/trpc/routers/skills.js.map +1 -0
- package/dist/trpc/routers/storage.d.ts +44 -0
- package/dist/trpc/routers/storage.d.ts.map +1 -0
- package/dist/trpc/routers/storage.js +153 -0
- package/dist/trpc/routers/storage.js.map +1 -0
- package/dist/trpc/routers/threads.d.ts +95 -0
- package/dist/trpc/routers/threads.d.ts.map +1 -0
- package/dist/trpc/routers/threads.js +150 -0
- package/dist/trpc/routers/threads.js.map +1 -0
- package/dist/trpc/routers/users.d.ts +79 -0
- package/dist/trpc/routers/users.d.ts.map +1 -0
- package/dist/trpc/routers/users.js +125 -0
- package/dist/trpc/routers/users.js.map +1 -0
- package/dist/trpc/routers/workflows.d.ts +681 -0
- package/dist/trpc/routers/workflows.d.ts.map +1 -0
- package/dist/trpc/routers/workflows.js +725 -0
- package/dist/trpc/routers/workflows.js.map +1 -0
- package/dist/trpc/routers/workspace.d.ts +127 -0
- package/dist/trpc/routers/workspace.d.ts.map +1 -0
- package/dist/trpc/routers/workspace.js +203 -0
- package/dist/trpc/routers/workspace.js.map +1 -0
- package/dist/unified-websocket-runner.d.ts +280 -0
- package/dist/unified-websocket-runner.d.ts.map +1 -0
- package/dist/unified-websocket-runner.js +3840 -0
- package/dist/unified-websocket-runner.js.map +1 -0
- package/dist/workspace-api.d.ts +14 -0
- package/dist/workspace-api.d.ts.map +1 -0
- package/dist/workspace-api.js +104 -0
- package/dist/workspace-api.js.map +1 -0
- package/dist/ws-adapter.d.ts +25 -0
- package/dist/ws-adapter.d.ts.map +1 -0
- package/dist/ws-adapter.js +63 -0
- package/dist/ws-adapter.js.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflows router — migrated from REST `/api/workflows*`.
|
|
3
|
+
*
|
|
4
|
+
* Retained on REST (file downloads):
|
|
5
|
+
* - GET /api/workflows/:id/dsl-export (text/plain file download)
|
|
6
|
+
* - POST /api/workflows/:id/gradio-export (501 stub, kept for API compat)
|
|
7
|
+
*
|
|
8
|
+
* Everything else is served via tRPC:
|
|
9
|
+
* - list, names, get, create, update, delete
|
|
10
|
+
* - run, autosave
|
|
11
|
+
* - tools, examples (list + search)
|
|
12
|
+
* - public (list + get) — publicProcedure (no auth required)
|
|
13
|
+
* - app (metadata for workflow app page)
|
|
14
|
+
* - generateName
|
|
15
|
+
* - versions (list, create, get, restore, delete)
|
|
16
|
+
*
|
|
17
|
+
* Auth note:
|
|
18
|
+
* - `public.*` endpoints use publicProcedure — no auth required, matches
|
|
19
|
+
* the legacy behaviour (public workflows are browsable without login).
|
|
20
|
+
* - All other endpoints use protectedProcedure.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
23
|
+
import nodePath from "node:path";
|
|
24
|
+
import { Workflow, WorkflowVersion, Job } from "@nodetool-ai/models";
|
|
25
|
+
import { PythonNodeExecutor } from "@nodetool-ai/runtime";
|
|
26
|
+
import { WorkflowRunner } from "@nodetool-ai/kernel";
|
|
27
|
+
import { loadPythonPackageMetadata } from "@nodetool-ai/node-sdk";
|
|
28
|
+
import { createLogger } from "@nodetool-ai/config";
|
|
29
|
+
import { ApiErrorCode } from "../../error-codes.js";
|
|
30
|
+
import { router, publicProcedure } from "../index.js";
|
|
31
|
+
import { protectedProcedure } from "../middleware.js";
|
|
32
|
+
import { throwApiError } from "../error-formatter.js";
|
|
33
|
+
import { listInput, listOutput, namesOutput, getInput, createInput, updateInput, deleteInput, deleteOutput, runInput, runOutput, autosaveInput, autosaveOutput, toolsInput, toolsOutput, examplesInput, examplesOutput, publicListInput, publicListOutput, publicGetInput, appInput, appOutput, generateNameInput, generateNameOutput, versionsListInput, versionsListOutput, versionCreateInput, versionGetInput, versionResponse, versionRestoreInput, versionDeleteInput, versionDeleteOutput, workflowResponse, graph as graphSchema } from "@nodetool-ai/protocol/api-schemas/workflows.js";
|
|
34
|
+
const log = createLogger("nodetool.websocket.trpc.workflows");
|
|
35
|
+
/**
|
|
36
|
+
* Validate a workflow graph against the wire schema. Returns the graph if it
|
|
37
|
+
* passes, or null if invalid — so list endpoints can still return the rest of
|
|
38
|
+
* the workflow row instead of failing the entire response.
|
|
39
|
+
*/
|
|
40
|
+
function summarizeGraphIssues(issues) {
|
|
41
|
+
const [first] = issues;
|
|
42
|
+
if (!first) {
|
|
43
|
+
return "unknown validation error";
|
|
44
|
+
}
|
|
45
|
+
const path = first.path.length > 0
|
|
46
|
+
? first.path.map((segment) => String(segment)).join(".")
|
|
47
|
+
: "<root>";
|
|
48
|
+
const suffix = issues.length > 1 ? ` (+${issues.length - 1} more)` : "";
|
|
49
|
+
return `${path}: ${first.message}${suffix}`;
|
|
50
|
+
}
|
|
51
|
+
function safeGraph(workflowId, raw) {
|
|
52
|
+
if (raw == null)
|
|
53
|
+
return null;
|
|
54
|
+
const parsed = graphSchema.safeParse(raw);
|
|
55
|
+
if (parsed.success)
|
|
56
|
+
return parsed.data;
|
|
57
|
+
log.warn(`Workflow ${workflowId} has an invalid graph; returning null: ${summarizeGraphIssues(parsed.error.issues)}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
// ── Rate-limit tracking for autosave ───────────────────────────────────────
|
|
61
|
+
const lastAutosaveTime = new Map();
|
|
62
|
+
const AUTOSAVE_RATE_LIMIT_MS = 30_000;
|
|
63
|
+
// ── toWorkflowResponse ─────────────────────────────────────────────────────
|
|
64
|
+
function toWorkflowResponse(workflow) {
|
|
65
|
+
return {
|
|
66
|
+
id: workflow.id,
|
|
67
|
+
access: workflow.access,
|
|
68
|
+
created_at: workflow.created_at ?? null,
|
|
69
|
+
updated_at: workflow.updated_at ?? null,
|
|
70
|
+
name: workflow.name,
|
|
71
|
+
tool_name: workflow.tool_name ?? null,
|
|
72
|
+
description: workflow.description ?? null,
|
|
73
|
+
tags: workflow.tags ?? [],
|
|
74
|
+
thumbnail: workflow.thumbnail ?? null,
|
|
75
|
+
thumbnail_url: workflow.thumbnail_url ?? null,
|
|
76
|
+
graph: safeGraph(workflow.id, workflow.graph),
|
|
77
|
+
input_schema: null,
|
|
78
|
+
output_schema: null,
|
|
79
|
+
settings: workflow.settings ?? null,
|
|
80
|
+
package_name: workflow.package_name ?? null,
|
|
81
|
+
path: workflow.path ?? null,
|
|
82
|
+
run_mode: workflow.run_mode ?? null,
|
|
83
|
+
workspace_id: workflow.workspace_id ?? null,
|
|
84
|
+
required_providers: null,
|
|
85
|
+
required_models: null,
|
|
86
|
+
html_app: workflow.html_app ?? null,
|
|
87
|
+
etag: workflow.getEtag() ?? null
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// ── toVersionResponse ──────────────────────────────────────────────────────
|
|
91
|
+
function toVersionResponse(v) {
|
|
92
|
+
return {
|
|
93
|
+
id: v.id,
|
|
94
|
+
workflow_id: v.workflow_id,
|
|
95
|
+
user_id: v.user_id,
|
|
96
|
+
name: v.name ?? null,
|
|
97
|
+
description: v.description ?? null,
|
|
98
|
+
graph: v.graph ?? null,
|
|
99
|
+
version: v.version,
|
|
100
|
+
save_type: v.save_type ?? "manual",
|
|
101
|
+
autosave_metadata: v.autosave_metadata ?? null,
|
|
102
|
+
created_at: v.created_at ?? null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// ── Example workflow helpers ────────────────────────────────────────────────
|
|
106
|
+
const EXAMPLES_THUMBNAILS_PREFIX = "/api/workflows/examples/thumbnails/";
|
|
107
|
+
function deriveAssetsDir(examplesDir) {
|
|
108
|
+
return nodePath.join(nodePath.dirname(nodePath.dirname(examplesDir)), "assets", nodePath.basename(examplesDir));
|
|
109
|
+
}
|
|
110
|
+
function buildExamplesFromDir(examplesDir) {
|
|
111
|
+
if (!existsSync(examplesDir))
|
|
112
|
+
return [];
|
|
113
|
+
const assetsDir = deriveAssetsDir(examplesDir);
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
const workflows = [];
|
|
116
|
+
let files;
|
|
117
|
+
try {
|
|
118
|
+
files = readdirSync(examplesDir)
|
|
119
|
+
.filter((f) => f.toLowerCase().endsWith(".json"))
|
|
120
|
+
.sort((a, b) => a.localeCompare(b));
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
try {
|
|
127
|
+
const raw = readFileSync(nodePath.join(examplesDir, file), "utf8");
|
|
128
|
+
const parsed = JSON.parse(raw);
|
|
129
|
+
const name = typeof parsed.name === "string"
|
|
130
|
+
? parsed.name
|
|
131
|
+
: file.replace(/\.json$/i, "");
|
|
132
|
+
const jpgFile = `${name}.jpg`;
|
|
133
|
+
const thumbnailUrl = existsSync(nodePath.join(assetsDir, jpgFile))
|
|
134
|
+
? `${EXAMPLES_THUMBNAILS_PREFIX}${encodeURIComponent(jpgFile)}`
|
|
135
|
+
: null;
|
|
136
|
+
workflows.push({
|
|
137
|
+
id: file,
|
|
138
|
+
access: "public",
|
|
139
|
+
created_at: now,
|
|
140
|
+
updated_at: now,
|
|
141
|
+
name,
|
|
142
|
+
tool_name: null,
|
|
143
|
+
description: typeof parsed.description === "string" ? parsed.description : "",
|
|
144
|
+
tags: Array.isArray(parsed.tags)
|
|
145
|
+
? parsed.tags.filter((t) => typeof t === "string")
|
|
146
|
+
: [],
|
|
147
|
+
thumbnail: thumbnailUrl ? jpgFile : null,
|
|
148
|
+
thumbnail_url: thumbnailUrl,
|
|
149
|
+
graph: { nodes: [], edges: [] },
|
|
150
|
+
input_schema: null,
|
|
151
|
+
output_schema: null,
|
|
152
|
+
settings: null,
|
|
153
|
+
package_name: typeof parsed.package_name === "string" ? parsed.package_name : null,
|
|
154
|
+
path: null,
|
|
155
|
+
run_mode: null,
|
|
156
|
+
workspace_id: null,
|
|
157
|
+
required_providers: null,
|
|
158
|
+
required_models: null,
|
|
159
|
+
html_app: null,
|
|
160
|
+
etag: null
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// skip invalid files
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return workflows;
|
|
168
|
+
}
|
|
169
|
+
function buildExampleWorkflows(apiOptions) {
|
|
170
|
+
if (apiOptions.examplesDir) {
|
|
171
|
+
return buildExamplesFromDir(apiOptions.examplesDir);
|
|
172
|
+
}
|
|
173
|
+
const loaded = loadPythonPackageMetadata({
|
|
174
|
+
roots: apiOptions.metadataRoots,
|
|
175
|
+
maxDepth: apiOptions.metadataMaxDepth
|
|
176
|
+
});
|
|
177
|
+
const now = new Date().toISOString();
|
|
178
|
+
const workflows = [];
|
|
179
|
+
for (const pkg of loaded.packages) {
|
|
180
|
+
if (!pkg.examples || pkg.examples.length === 0)
|
|
181
|
+
continue;
|
|
182
|
+
for (const ex of pkg.examples) {
|
|
183
|
+
const meta = ex;
|
|
184
|
+
workflows.push({
|
|
185
|
+
id: meta.id ?? "",
|
|
186
|
+
access: "public",
|
|
187
|
+
created_at: now,
|
|
188
|
+
updated_at: now,
|
|
189
|
+
name: meta.name,
|
|
190
|
+
tool_name: null,
|
|
191
|
+
description: meta.description ?? "",
|
|
192
|
+
tags: meta.tags ?? [],
|
|
193
|
+
thumbnail: null,
|
|
194
|
+
thumbnail_url: null,
|
|
195
|
+
graph: { nodes: [], edges: [] },
|
|
196
|
+
input_schema: null,
|
|
197
|
+
output_schema: null,
|
|
198
|
+
settings: null,
|
|
199
|
+
package_name: pkg.name,
|
|
200
|
+
path: null,
|
|
201
|
+
run_mode: null,
|
|
202
|
+
workspace_id: null,
|
|
203
|
+
required_providers: null,
|
|
204
|
+
required_models: null,
|
|
205
|
+
html_app: null,
|
|
206
|
+
etag: null
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return workflows;
|
|
211
|
+
}
|
|
212
|
+
function loadExampleGraph(packageName, exampleName, apiOptions) {
|
|
213
|
+
const loaded = loadPythonPackageMetadata({
|
|
214
|
+
roots: apiOptions.metadataRoots,
|
|
215
|
+
maxDepth: apiOptions.metadataMaxDepth
|
|
216
|
+
});
|
|
217
|
+
const pkg = loaded.packages.find((p) => p.name === packageName);
|
|
218
|
+
if (!pkg?.sourceFolder)
|
|
219
|
+
return null;
|
|
220
|
+
const examplePath = nodePath.join(pkg.sourceFolder, "nodetool", "examples", packageName, `${exampleName}.json`);
|
|
221
|
+
try {
|
|
222
|
+
const raw = readFileSync(examplePath, "utf8");
|
|
223
|
+
return JSON.parse(raw);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ── deriveWorkflowName ─────────────────────────────────────────────────────
|
|
230
|
+
function deriveWorkflowName(workflow) {
|
|
231
|
+
const graph = workflow.graph;
|
|
232
|
+
const nodes = graph?.nodes ?? [];
|
|
233
|
+
if (nodes.length === 0) {
|
|
234
|
+
return workflow.name || "Untitled Workflow";
|
|
235
|
+
}
|
|
236
|
+
const categories = new Set();
|
|
237
|
+
for (const n of nodes) {
|
|
238
|
+
if (typeof n.type === "string") {
|
|
239
|
+
const parts = n.type.split(".");
|
|
240
|
+
if (parts.length >= 2 && parts[1]) {
|
|
241
|
+
categories.add(parts[1]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const segments = Array.from(categories).slice(0, 3);
|
|
246
|
+
if (segments.length === 0) {
|
|
247
|
+
return workflow.name || "Untitled Workflow";
|
|
248
|
+
}
|
|
249
|
+
const label = segments
|
|
250
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
251
|
+
.join(" + ");
|
|
252
|
+
return `${label} Workflow`;
|
|
253
|
+
}
|
|
254
|
+
// ── Router ─────────────────────────────────────────────────────────────────
|
|
255
|
+
export const workflowsRouter = router({
|
|
256
|
+
// ── list (GET /api/workflows) ─────────────────────────────────────────────
|
|
257
|
+
list: protectedProcedure
|
|
258
|
+
.input(listInput)
|
|
259
|
+
.output(listOutput)
|
|
260
|
+
.query(async ({ ctx, input }) => {
|
|
261
|
+
const [workflows, cursor] = await Workflow.paginate(ctx.userId, {
|
|
262
|
+
limit: input.limit,
|
|
263
|
+
runMode: input.run_mode
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
workflows: workflows.map((w) => toWorkflowResponse(w)),
|
|
267
|
+
next: cursor || null
|
|
268
|
+
};
|
|
269
|
+
}),
|
|
270
|
+
// ── names (GET /api/workflows/names) ─────────────────────────────────────
|
|
271
|
+
names: protectedProcedure
|
|
272
|
+
.output(namesOutput)
|
|
273
|
+
.query(async ({ ctx }) => {
|
|
274
|
+
const [workflows] = await Workflow.paginate(ctx.userId, { limit: 1000 });
|
|
275
|
+
const names = {};
|
|
276
|
+
for (const wf of workflows)
|
|
277
|
+
names[wf.id] = wf.name;
|
|
278
|
+
return names;
|
|
279
|
+
}),
|
|
280
|
+
// ── get (GET /api/workflows/:id) ─────────────────────────────────────────
|
|
281
|
+
get: protectedProcedure
|
|
282
|
+
.input(getInput)
|
|
283
|
+
.output(workflowResponse)
|
|
284
|
+
.query(async ({ ctx, input }) => {
|
|
285
|
+
const workflow = (await Workflow.get(input.id));
|
|
286
|
+
if (!workflow)
|
|
287
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
288
|
+
if (workflow.access !== "public" && workflow.user_id !== ctx.userId) {
|
|
289
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
290
|
+
}
|
|
291
|
+
return toWorkflowResponse(workflow);
|
|
292
|
+
}),
|
|
293
|
+
// ── create (POST /api/workflows) ─────────────────────────────────────────
|
|
294
|
+
create: protectedProcedure
|
|
295
|
+
.input(createInput)
|
|
296
|
+
.output(workflowResponse)
|
|
297
|
+
.mutation(async ({ ctx, input }) => {
|
|
298
|
+
if (!input.graph || !Array.isArray(input.graph.nodes) || !Array.isArray(input.graph.edges)) {
|
|
299
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "graph is required and must have nodes and edges arrays");
|
|
300
|
+
}
|
|
301
|
+
let graph = input.graph;
|
|
302
|
+
// Optionally seed from example
|
|
303
|
+
if (input.from_example_package &&
|
|
304
|
+
input.from_example_name &&
|
|
305
|
+
(!graph || graph.nodes?.length === 0)) {
|
|
306
|
+
const example = loadExampleGraph(input.from_example_package, input.from_example_name, ctx.apiOptions);
|
|
307
|
+
if (example?.graph) {
|
|
308
|
+
graph = example.graph;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const workflow = (await Workflow.create({
|
|
312
|
+
user_id: ctx.userId,
|
|
313
|
+
name: input.name,
|
|
314
|
+
tool_name: input.tool_name ?? null,
|
|
315
|
+
package_name: input.package_name ?? null,
|
|
316
|
+
path: input.path ?? null,
|
|
317
|
+
tags: input.tags ?? [],
|
|
318
|
+
description: input.description ?? "",
|
|
319
|
+
thumbnail: input.thumbnail ?? null,
|
|
320
|
+
thumbnail_url: input.thumbnail_url ?? null,
|
|
321
|
+
access: input.access === "public" ? "public" : "private",
|
|
322
|
+
graph,
|
|
323
|
+
settings: input.settings ?? null,
|
|
324
|
+
run_mode: input.run_mode ?? "workflow",
|
|
325
|
+
workspace_id: input.workspace_id ?? null,
|
|
326
|
+
html_app: input.html_app ?? null
|
|
327
|
+
}));
|
|
328
|
+
return toWorkflowResponse(workflow);
|
|
329
|
+
}),
|
|
330
|
+
// ── update (PUT /api/workflows/:id) ──────────────────────────────────────
|
|
331
|
+
update: protectedProcedure
|
|
332
|
+
.input(updateInput)
|
|
333
|
+
.output(workflowResponse)
|
|
334
|
+
.mutation(async ({ ctx, input }) => {
|
|
335
|
+
if (!input.graph || !Array.isArray(input.graph.nodes) || !Array.isArray(input.graph.edges)) {
|
|
336
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, "graph is required and must have nodes and edges arrays");
|
|
337
|
+
}
|
|
338
|
+
const graph = input.graph;
|
|
339
|
+
const existing = (await Workflow.get(input.id));
|
|
340
|
+
if (existing && existing.user_id !== ctx.userId) {
|
|
341
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
342
|
+
}
|
|
343
|
+
if (existing) {
|
|
344
|
+
existing.name = input.name;
|
|
345
|
+
existing.tool_name = input.tool_name ?? null;
|
|
346
|
+
existing.description = input.description ?? "";
|
|
347
|
+
existing.tags = input.tags ?? [];
|
|
348
|
+
existing.package_name = input.package_name ?? null;
|
|
349
|
+
if (input.thumbnail !== undefined)
|
|
350
|
+
existing.thumbnail = input.thumbnail;
|
|
351
|
+
existing.access = input.access === "public" ? "public" : "private";
|
|
352
|
+
existing.graph = graph;
|
|
353
|
+
existing.settings = input.settings ?? null;
|
|
354
|
+
if (input.run_mode !== undefined && input.run_mode !== null)
|
|
355
|
+
existing.run_mode = input.run_mode;
|
|
356
|
+
existing.workspace_id = input.workspace_id ?? null;
|
|
357
|
+
existing.html_app = input.html_app ?? null;
|
|
358
|
+
await existing.save();
|
|
359
|
+
return toWorkflowResponse(existing);
|
|
360
|
+
}
|
|
361
|
+
// Upsert: create if doesn't exist
|
|
362
|
+
const workflow = (await Workflow.create({
|
|
363
|
+
id: input.id,
|
|
364
|
+
user_id: ctx.userId,
|
|
365
|
+
name: input.name,
|
|
366
|
+
tool_name: input.tool_name ?? null,
|
|
367
|
+
package_name: input.package_name ?? null,
|
|
368
|
+
path: input.path ?? null,
|
|
369
|
+
tags: input.tags ?? [],
|
|
370
|
+
description: input.description ?? "",
|
|
371
|
+
thumbnail: input.thumbnail ?? null,
|
|
372
|
+
thumbnail_url: input.thumbnail_url ?? null,
|
|
373
|
+
access: input.access === "public" ? "public" : "private",
|
|
374
|
+
graph,
|
|
375
|
+
settings: input.settings ?? null,
|
|
376
|
+
run_mode: input.run_mode ?? "workflow",
|
|
377
|
+
workspace_id: input.workspace_id ?? null,
|
|
378
|
+
html_app: input.html_app ?? null
|
|
379
|
+
}));
|
|
380
|
+
return toWorkflowResponse(workflow);
|
|
381
|
+
}),
|
|
382
|
+
// ── delete (DELETE /api/workflows/:id) ───────────────────────────────────
|
|
383
|
+
delete: protectedProcedure
|
|
384
|
+
.input(deleteInput)
|
|
385
|
+
.output(deleteOutput)
|
|
386
|
+
.mutation(async ({ ctx, input }) => {
|
|
387
|
+
const workflow = (await Workflow.get(input.id));
|
|
388
|
+
if (!workflow)
|
|
389
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
390
|
+
if (workflow.user_id !== ctx.userId) {
|
|
391
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
392
|
+
}
|
|
393
|
+
await workflow.delete();
|
|
394
|
+
return { ok: true };
|
|
395
|
+
}),
|
|
396
|
+
// ── run (POST /api/workflows/:id/run) ────────────────────────────────────
|
|
397
|
+
run: protectedProcedure
|
|
398
|
+
.input(runInput)
|
|
399
|
+
.output(runOutput)
|
|
400
|
+
.mutation(async ({ ctx, input }) => {
|
|
401
|
+
const workflow = await Workflow.find(ctx.userId, input.id);
|
|
402
|
+
if (!workflow)
|
|
403
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
404
|
+
const runMode = workflow.run_mode ?? "workflow";
|
|
405
|
+
if (runMode !== "workflow") {
|
|
406
|
+
throwApiError(ApiErrorCode.INVALID_INPUT, `Workflow run mode "${runMode}" is not supported by the standalone backend`);
|
|
407
|
+
}
|
|
408
|
+
const params = input.params ?? {};
|
|
409
|
+
const graph = workflow.getGraph();
|
|
410
|
+
const runnableGraph = {
|
|
411
|
+
nodes: graph.nodes.map((node) => {
|
|
412
|
+
const record = node;
|
|
413
|
+
return {
|
|
414
|
+
...record,
|
|
415
|
+
id: String(record.id ?? ""),
|
|
416
|
+
type: String(record.type ?? ""),
|
|
417
|
+
properties: (record.properties ?? record.data ?? {})
|
|
418
|
+
};
|
|
419
|
+
}),
|
|
420
|
+
edges: graph.edges.map((edge) => {
|
|
421
|
+
const record = edge;
|
|
422
|
+
return {
|
|
423
|
+
...record,
|
|
424
|
+
id: typeof record.id === "string" || record.id == null
|
|
425
|
+
? record.id
|
|
426
|
+
: String(record.id),
|
|
427
|
+
source: String(record.source ?? ""),
|
|
428
|
+
target: String(record.target ?? ""),
|
|
429
|
+
sourceHandle: String(record.sourceHandle ?? ""),
|
|
430
|
+
targetHandle: String(record.targetHandle ?? ""),
|
|
431
|
+
edge_type: record.edge_type === "control" ? "control" : "data"
|
|
432
|
+
};
|
|
433
|
+
})
|
|
434
|
+
};
|
|
435
|
+
const registry = ctx.registry;
|
|
436
|
+
const pythonBridge = ctx.pythonBridge;
|
|
437
|
+
const getPythonBridgeReady = ctx.getPythonBridgeReady;
|
|
438
|
+
const hasPythonNode = runnableGraph.nodes.some((node) => {
|
|
439
|
+
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
440
|
+
return (nodeType !== "" &&
|
|
441
|
+
Boolean(registry.getMetadata(nodeType)) &&
|
|
442
|
+
!registry.has(nodeType));
|
|
443
|
+
});
|
|
444
|
+
if (hasPythonNode) {
|
|
445
|
+
if (!getPythonBridgeReady()) {
|
|
446
|
+
await pythonBridge.ensureConnected();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const resolveExecutor = (node) => {
|
|
450
|
+
if (registry.has(node.type)) {
|
|
451
|
+
return registry.resolve(node);
|
|
452
|
+
}
|
|
453
|
+
if (getPythonBridgeReady() && pythonBridge.hasNodeType(node.type)) {
|
|
454
|
+
const meta = pythonBridge
|
|
455
|
+
.getNodeMetadata()
|
|
456
|
+
.find((n) => n.node_type === node.type);
|
|
457
|
+
const props = (node.properties ?? {});
|
|
458
|
+
return new PythonNodeExecutor(pythonBridge, node.type, props, Object.fromEntries((meta?.outputs ?? []).map((o) => [o.name, o.type.type])), meta?.required_settings ?? []);
|
|
459
|
+
}
|
|
460
|
+
return registry.resolve(node);
|
|
461
|
+
};
|
|
462
|
+
const job = await Job.create({
|
|
463
|
+
workflow_id: input.id,
|
|
464
|
+
user_id: ctx.userId,
|
|
465
|
+
status: "running",
|
|
466
|
+
params,
|
|
467
|
+
graph: runnableGraph
|
|
468
|
+
});
|
|
469
|
+
const runner = new WorkflowRunner(job.id, { resolveExecutor });
|
|
470
|
+
const result = await runner.run({ job_id: job.id, workflow_id: input.id, params }, runnableGraph);
|
|
471
|
+
if (result.status === "completed") {
|
|
472
|
+
job.markCompleted();
|
|
473
|
+
}
|
|
474
|
+
else if (result.status === "cancelled") {
|
|
475
|
+
job.markCancelled();
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
job.markFailed(result.error ?? "Workflow run failed");
|
|
479
|
+
}
|
|
480
|
+
await job.save();
|
|
481
|
+
return {
|
|
482
|
+
job_id: job.id,
|
|
483
|
+
workflow_id: input.id,
|
|
484
|
+
status: result.status,
|
|
485
|
+
outputs: result.outputs ?? null,
|
|
486
|
+
error: result.error ?? null,
|
|
487
|
+
message_count: result.messages.length,
|
|
488
|
+
background: input.background ?? false
|
|
489
|
+
};
|
|
490
|
+
}),
|
|
491
|
+
// ── autosave (POST/PUT /api/workflows/:id/autosave) ───────────────────────
|
|
492
|
+
autosave: protectedProcedure
|
|
493
|
+
.input(autosaveInput)
|
|
494
|
+
.output(autosaveOutput)
|
|
495
|
+
.mutation(async ({ ctx, input }) => {
|
|
496
|
+
const workflow = (await Workflow.get(input.id));
|
|
497
|
+
if (!workflow)
|
|
498
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
499
|
+
if (workflow.user_id !== ctx.userId) {
|
|
500
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
501
|
+
}
|
|
502
|
+
const force = input.force === true;
|
|
503
|
+
const maxVersions = typeof input.max_versions === "number" ? input.max_versions : 10;
|
|
504
|
+
// Rate-limit
|
|
505
|
+
if (!force) {
|
|
506
|
+
const last = lastAutosaveTime.get(input.id);
|
|
507
|
+
if (last !== undefined && Date.now() - last < AUTOSAVE_RATE_LIMIT_MS) {
|
|
508
|
+
return {
|
|
509
|
+
version: null,
|
|
510
|
+
message: "Autosave skipped (rate limited)",
|
|
511
|
+
skipped: true
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
workflow.graph = input.graph;
|
|
516
|
+
if (input.name !== undefined)
|
|
517
|
+
workflow.name = input.name;
|
|
518
|
+
if (input.description !== undefined)
|
|
519
|
+
workflow.description = input.description;
|
|
520
|
+
if (input.access === "public" || input.access === "private")
|
|
521
|
+
workflow.access = input.access;
|
|
522
|
+
await workflow.save();
|
|
523
|
+
lastAutosaveTime.set(input.id, Date.now());
|
|
524
|
+
let version = null;
|
|
525
|
+
try {
|
|
526
|
+
const nextVer = await WorkflowVersion.nextVersion(input.id);
|
|
527
|
+
const wv = new WorkflowVersion({
|
|
528
|
+
workflow_id: input.id,
|
|
529
|
+
user_id: ctx.userId,
|
|
530
|
+
graph: input.graph,
|
|
531
|
+
version: nextVer,
|
|
532
|
+
save_type: "autosave",
|
|
533
|
+
name: workflow.name,
|
|
534
|
+
description: workflow.description
|
|
535
|
+
});
|
|
536
|
+
await wv.save();
|
|
537
|
+
version = {
|
|
538
|
+
id: wv.id,
|
|
539
|
+
version: wv.version,
|
|
540
|
+
workflow_id: wv.workflow_id,
|
|
541
|
+
save_type: wv.save_type ?? "autosave",
|
|
542
|
+
created_at: wv.created_at ?? null
|
|
543
|
+
};
|
|
544
|
+
await WorkflowVersion.pruneOldVersions(input.id, maxVersions);
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
// non-fatal — version table may not exist
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
version,
|
|
551
|
+
message: "Autosaved successfully",
|
|
552
|
+
skipped: false
|
|
553
|
+
};
|
|
554
|
+
}),
|
|
555
|
+
// ── tools (GET /api/workflows/tools) ─────────────────────────────────────
|
|
556
|
+
tools: protectedProcedure
|
|
557
|
+
.input(toolsInput)
|
|
558
|
+
.output(toolsOutput)
|
|
559
|
+
.query(async ({ ctx, input }) => {
|
|
560
|
+
const [workflows] = await Workflow.paginateTools(ctx.userId, {
|
|
561
|
+
limit: input.limit
|
|
562
|
+
});
|
|
563
|
+
return {
|
|
564
|
+
workflows: workflows.map((w) => ({
|
|
565
|
+
name: w.name,
|
|
566
|
+
tool_name: w.tool_name ?? null,
|
|
567
|
+
description: w.description ?? null
|
|
568
|
+
})),
|
|
569
|
+
next: null
|
|
570
|
+
};
|
|
571
|
+
}),
|
|
572
|
+
// ── examples (GET /api/workflows/examples) ────────────────────────────────
|
|
573
|
+
examples: protectedProcedure
|
|
574
|
+
.input(examplesInput)
|
|
575
|
+
.output(examplesOutput)
|
|
576
|
+
.query(async ({ ctx, input }) => {
|
|
577
|
+
const workflows = buildExampleWorkflows(ctx.apiOptions);
|
|
578
|
+
const query = (input.query ?? "").toLowerCase().trim();
|
|
579
|
+
const filtered = query
|
|
580
|
+
? workflows.filter((w) => {
|
|
581
|
+
const wf = w;
|
|
582
|
+
const name = String(wf.name ?? "").toLowerCase();
|
|
583
|
+
const desc = String(wf.description ?? "").toLowerCase();
|
|
584
|
+
const tags = wf.tags ?? [];
|
|
585
|
+
return (name.includes(query) ||
|
|
586
|
+
desc.includes(query) ||
|
|
587
|
+
tags.some((t) => t.toLowerCase().includes(query)));
|
|
588
|
+
})
|
|
589
|
+
: workflows;
|
|
590
|
+
return { workflows: filtered, next: null };
|
|
591
|
+
}),
|
|
592
|
+
// ── public workflows ──────────────────────────────────────────────────────
|
|
593
|
+
public: router({
|
|
594
|
+
list: publicProcedure
|
|
595
|
+
.input(publicListInput)
|
|
596
|
+
.output(publicListOutput)
|
|
597
|
+
.query(async ({ input }) => {
|
|
598
|
+
const [workflows] = await Workflow.paginatePublic({ limit: input.limit });
|
|
599
|
+
return {
|
|
600
|
+
workflows: workflows.map((w) => toWorkflowResponse(w)),
|
|
601
|
+
next: null
|
|
602
|
+
};
|
|
603
|
+
}),
|
|
604
|
+
get: publicProcedure
|
|
605
|
+
.input(publicGetInput)
|
|
606
|
+
.output(workflowResponse)
|
|
607
|
+
.query(async ({ input }) => {
|
|
608
|
+
const workflow = (await Workflow.get(input.id));
|
|
609
|
+
if (!workflow || workflow.access !== "public") {
|
|
610
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
611
|
+
}
|
|
612
|
+
return toWorkflowResponse(workflow);
|
|
613
|
+
})
|
|
614
|
+
}),
|
|
615
|
+
// ── app (GET /api/workflows/:id/app) ─────────────────────────────────────
|
|
616
|
+
app: protectedProcedure
|
|
617
|
+
.input(appInput)
|
|
618
|
+
.output(appOutput)
|
|
619
|
+
.query(async ({ ctx, input }) => {
|
|
620
|
+
const baseUrl = input.baseUrl ?? ctx.apiOptions.baseUrl ?? "http://127.0.0.1:7777";
|
|
621
|
+
return {
|
|
622
|
+
workflow_id: input.id,
|
|
623
|
+
api_url: baseUrl
|
|
624
|
+
};
|
|
625
|
+
}),
|
|
626
|
+
// ── generateName (POST /api/workflows/:id/generate-name) ─────────────────
|
|
627
|
+
generateName: protectedProcedure
|
|
628
|
+
.input(generateNameInput)
|
|
629
|
+
.output(generateNameOutput)
|
|
630
|
+
.mutation(async ({ ctx, input }) => {
|
|
631
|
+
const workflow = (await Workflow.get(input.id));
|
|
632
|
+
if (!workflow)
|
|
633
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
634
|
+
if (workflow.user_id !== ctx.userId) {
|
|
635
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
636
|
+
}
|
|
637
|
+
const name = deriveWorkflowName(workflow);
|
|
638
|
+
return { name };
|
|
639
|
+
}),
|
|
640
|
+
// ── versions ──────────────────────────────────────────────────────────────
|
|
641
|
+
versions: router({
|
|
642
|
+
// GET /api/workflows/:id/versions
|
|
643
|
+
list: protectedProcedure
|
|
644
|
+
.input(versionsListInput)
|
|
645
|
+
.output(versionsListOutput)
|
|
646
|
+
.query(async ({ ctx, input }) => {
|
|
647
|
+
// Check ownership via the parent workflow
|
|
648
|
+
const workflow = (await Workflow.get(input.id));
|
|
649
|
+
if (!workflow || workflow.user_id !== ctx.userId) {
|
|
650
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
651
|
+
}
|
|
652
|
+
const versions = await WorkflowVersion.listForWorkflow(input.id, {
|
|
653
|
+
limit: input.limit
|
|
654
|
+
});
|
|
655
|
+
return { versions: versions.map(toVersionResponse) };
|
|
656
|
+
}),
|
|
657
|
+
// POST /api/workflows/:id/versions
|
|
658
|
+
create: protectedProcedure
|
|
659
|
+
.input(versionCreateInput)
|
|
660
|
+
.output(versionResponse)
|
|
661
|
+
.mutation(async ({ ctx, input }) => {
|
|
662
|
+
const workflow = (await Workflow.get(input.id));
|
|
663
|
+
if (!workflow)
|
|
664
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
665
|
+
if (workflow.user_id !== ctx.userId) {
|
|
666
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
667
|
+
}
|
|
668
|
+
const nextVer = await WorkflowVersion.nextVersion(input.id);
|
|
669
|
+
const version = (await WorkflowVersion.create({
|
|
670
|
+
workflow_id: input.id,
|
|
671
|
+
user_id: ctx.userId,
|
|
672
|
+
name: input.name ?? null,
|
|
673
|
+
description: input.description ?? null,
|
|
674
|
+
graph: workflow.graph,
|
|
675
|
+
version: nextVer
|
|
676
|
+
}));
|
|
677
|
+
return toVersionResponse(version);
|
|
678
|
+
}),
|
|
679
|
+
// GET /api/workflows/:id/versions/:version
|
|
680
|
+
get: protectedProcedure
|
|
681
|
+
.input(versionGetInput)
|
|
682
|
+
.output(versionResponse)
|
|
683
|
+
.query(async ({ ctx, input }) => {
|
|
684
|
+
const workflow = (await Workflow.get(input.id));
|
|
685
|
+
if (!workflow || workflow.user_id !== ctx.userId) {
|
|
686
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
687
|
+
}
|
|
688
|
+
const version = await WorkflowVersion.findByVersion(input.id, input.version);
|
|
689
|
+
if (!version)
|
|
690
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Version not found");
|
|
691
|
+
return toVersionResponse(version);
|
|
692
|
+
}),
|
|
693
|
+
// POST /api/workflows/:id/versions/:version/restore
|
|
694
|
+
restore: protectedProcedure
|
|
695
|
+
.input(versionRestoreInput)
|
|
696
|
+
.output(workflowResponse)
|
|
697
|
+
.mutation(async ({ ctx, input }) => {
|
|
698
|
+
const workflow = (await Workflow.get(input.id));
|
|
699
|
+
if (!workflow || workflow.user_id !== ctx.userId) {
|
|
700
|
+
throwApiError(ApiErrorCode.WORKFLOW_NOT_FOUND, "Workflow not found");
|
|
701
|
+
}
|
|
702
|
+
const version = await WorkflowVersion.findByVersion(input.id, input.version);
|
|
703
|
+
if (!version)
|
|
704
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Version not found");
|
|
705
|
+
workflow.graph = version.graph;
|
|
706
|
+
await workflow.save();
|
|
707
|
+
return toWorkflowResponse(workflow);
|
|
708
|
+
}),
|
|
709
|
+
// DELETE /api/workflows/:id/versions/:versionId
|
|
710
|
+
delete: protectedProcedure
|
|
711
|
+
.input(versionDeleteInput)
|
|
712
|
+
.output(versionDeleteOutput)
|
|
713
|
+
.mutation(async ({ ctx, input }) => {
|
|
714
|
+
const version = (await WorkflowVersion.get(input.version_id));
|
|
715
|
+
if (!version)
|
|
716
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Version not found");
|
|
717
|
+
if (version.user_id !== ctx.userId) {
|
|
718
|
+
throwApiError(ApiErrorCode.NOT_FOUND, "Version not found");
|
|
719
|
+
}
|
|
720
|
+
await version.delete();
|
|
721
|
+
return { ok: true };
|
|
722
|
+
})
|
|
723
|
+
})
|
|
724
|
+
});
|
|
725
|
+
//# sourceMappingURL=workflows.js.map
|