/** * Automation2 API * Node types and graph execution for n8n-style flows. */ import type { ApiRequestOptions } from '../hooks/useApi'; const LOG = '[Automation2]'; // ============================================================================ // TYPES // ============================================================================ export interface NodeTypeParameter { name: string; type: string; required?: boolean; description?: string; default?: unknown; } export interface NodeType { id: string; category: string; label: string; description: string; parameters: NodeTypeParameter[]; inputs: number; outputs: number; /** Labels per output (e.g. ["Ja", "Nein"] for flow.ifElse) */ outputLabels?: string[]; executor: string; meta?: { icon?: string; color?: string; method?: string; action?: string; }; } export interface NodeTypeCategory { id: string; label: Record | string; } export interface NodeTypesResponse { nodeTypes: NodeType[]; categories: NodeTypeCategory[]; } export interface Automation2GraphNode { id: string; type: string; parameters?: Record; } export interface Automation2Connection { source: string; target: string; sourceOutput?: number; targetInput?: number; } export interface Automation2Graph { nodes: Automation2GraphNode[]; connections: Automation2Connection[]; } export interface ExecuteGraphResponse { success: boolean; nodeOutputs?: Record; error?: string; stopped?: boolean; failedNode?: string; paused?: boolean; taskId?: string; runId?: string; nodeId?: string; } /** Entry point / start configured outside the canvas (manual, form, schedule, …) */ export interface WorkflowEntryPoint { id: string; kind: string; category: 'on_demand' | 'always_on'; enabled: boolean; title: Record | string; description?: Record; config: Record; } export interface Automation2Workflow { id: string; label: string; graph: Automation2Graph; active?: boolean; /** Entry points (Starts) — how this workflow may be invoked */ invocations?: WorkflowEntryPoint[]; /** Enriched: run count */ runCount?: number; /** Enriched: has active (running/paused) run */ isRunning?: boolean; /** Enriched: status of active run */ runStatus?: string; /** Enriched: nodeId where workflow is stuck (paused) */ stuckAtNodeId?: string; /** Enriched: human-readable label for stuck node */ stuckAtNodeLabel?: string; /** Enriched: created timestamp (seconds) */ createdAt?: number; /** Enriched: last run started timestamp (seconds) */ lastStartedAt?: number; } // ============================================================================ // API FUNCTIONS // ============================================================================ export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; /** * Fetch node types for the flow builder (backend-driven). * GET /api/automation2/{instanceId}/node-types?language=de */ export async function fetchNodeTypes( request: ApiRequestFunction, instanceId: string, language = 'de' ): Promise { console.log(`${LOG} fetchNodeTypes: instanceId=${instanceId} language=${language}`); const data = await request({ url: `/api/automation2/${instanceId}/node-types`, method: 'get', params: { language }, }); const nodeTypes = data?.nodeTypes ?? []; const categories = data?.categories ?? []; console.log(`${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories`); return { nodeTypes, categories }; } /** * Execute an automation2 graph. * POST /api/automation2/{instanceId}/execute */ export interface ExecuteGraphOptions { /** Use a configured start on the saved workflow */ entryPointId?: string; /** Full run envelope (overrides entry point mapping) */ runEnvelope?: Record; /** Merged into envelope.payload */ payload?: Record; } export async function executeGraph( request: ApiRequestFunction, instanceId: string, graph: Automation2Graph, workflowId?: string, options?: ExecuteGraphOptions ): Promise { console.log( `${LOG} executeGraph request: instanceId=${instanceId} workflowId=${workflowId} nodes=${graph.nodes.length} connections=${graph.connections.length}`, { nodes: graph.nodes, connections: graph.connections, options } ); const start = performance.now(); try { const data: Record = { graph, workflowId }; if (options?.entryPointId) data.entryPointId = options.entryPointId; if (options?.runEnvelope) data.runEnvelope = options.runEnvelope; if (options?.payload && Object.keys(options.payload).length > 0) data.payload = options.payload; const result = await request({ url: `/api/automation2/${instanceId}/execute`, method: 'post', data, }); const ms = Math.round(performance.now() - start); console.log( `${LOG} executeGraph response (${ms}ms): success=${result?.success} error=${result?.error ?? 'none'} nodeOutputs_keys=${Object.keys(result?.nodeOutputs ?? {}).join(',')} failedNode=${result?.failedNode ?? '-'}`, result ); return result; } catch (err) { const ms = Math.round(performance.now() - start); console.error( `${LOG} executeGraph FAILED (${ms}ms): instanceId=${instanceId}`, err ); throw err; } } // ------------------------------------------------------------------------- // Workflows CRUD // ------------------------------------------------------------------------- export async function fetchWorkflows( request: ApiRequestFunction, instanceId: string, params?: { active?: boolean } ): Promise { const data = await request({ url: `/api/automation2/${instanceId}/workflows`, method: 'get', params: params?.active !== undefined ? { active: params.active } : undefined, }); return data?.workflows ?? []; } export async function fetchWorkflow( request: ApiRequestFunction, instanceId: string, workflowId: string ): Promise { return await request({ url: `/api/automation2/${instanceId}/workflows/${workflowId}`, method: 'get', }); } export async function createWorkflow( request: ApiRequestFunction, instanceId: string, body: { label: string; graph: Automation2Graph; invocations?: WorkflowEntryPoint[] } ): Promise { return await request({ url: `/api/automation2/${instanceId}/workflows`, method: 'post', data: body, }); } export async function updateWorkflow( request: ApiRequestFunction, instanceId: string, workflowId: string, body: { label?: string; graph?: Automation2Graph; invocations?: WorkflowEntryPoint[]; active?: boolean; } ): Promise { return await request({ url: `/api/automation2/${instanceId}/workflows/${workflowId}`, method: 'put', data: body, }); } export async function deleteWorkflow( request: ApiRequestFunction, instanceId: string, workflowId: string ): Promise { await request({ url: `/api/automation2/${instanceId}/workflows/${workflowId}`, method: 'delete', }); } export interface Automation2Run { id: string; workflowId: string; status: string; nodeOutputs?: Record; currentNodeId?: string; } export async function fetchWorkflowRuns( request: ApiRequestFunction, instanceId: string, workflowId: string ): Promise { const data = await request({ url: `/api/automation2/${instanceId}/workflows/${workflowId}/runs`, method: 'get', }); return data?.runs ?? []; } export interface CompletedRun extends Automation2Run { workflowLabel?: string; _modifiedAt?: number; _createdAt?: number; } export async function fetchCompletedRuns( request: ApiRequestFunction, instanceId: string, limit = 20 ): Promise { const data = await request({ url: `/api/automation2/${instanceId}/runs/completed`, method: 'get', params: { limit }, }); return data?.runs ?? []; } // ------------------------------------------------------------------------- // Tasks // ------------------------------------------------------------------------- export interface Automation2Task { id: string; runId: string; workflowId: string; nodeId: string; nodeType: string; config: Record; status: string; result?: Record; /** Workflow label (enriched by API) */ workflowLabel?: string; /** Unix timestamp ms (from sysCreatedAt) */ createdAt?: number; /** Optional due date - configurable in future */ dueAt?: number; } export async function fetchTasks( request: ApiRequestFunction, instanceId: string, params?: { workflowId?: string; status?: string } ): Promise { const data = await request({ url: `/api/automation2/${instanceId}/tasks`, method: 'get', params, }); return data?.tasks ?? []; } export async function completeTask( request: ApiRequestFunction, instanceId: string, taskId: string, result: Record ): Promise { return await request({ url: `/api/automation2/${instanceId}/tasks/${taskId}/complete`, method: 'post', data: { result }, }); } // ------------------------------------------------------------------------- // Connections and Browse (for Email/SharePoint node config) // ------------------------------------------------------------------------- export interface UserConnection { id: string; authority: string; externalUsername?: string; externalEmail?: string; status: string; } export async function fetchConnections( request: ApiRequestFunction, instanceId: string ): Promise { const data = await request({ url: `/api/automation2/${instanceId}/connections`, method: 'get', }); return data?.connections ?? []; } export interface ConnectionService { service: string; label: string; icon: string; } export async function fetchConnectionServices( request: ApiRequestFunction, instanceId: string, connectionId: string ): Promise { const data = await request({ url: `/api/automation2/${instanceId}/connections/${connectionId}/services`, method: 'get', }); return data?.services ?? []; } export interface BrowseEntry { name: string; path: string; isFolder: boolean; size?: number; mimeType?: string; metadata?: Record; } export async function fetchBrowse( request: ApiRequestFunction, instanceId: string, connectionId: string, service: string, path = '/' ): Promise<{ items: BrowseEntry[]; path: string; service: string }> { const data = await request({ url: `/api/automation2/${instanceId}/connections/${connectionId}/browse`, method: 'get', params: { service, path }, }); return { items: data?.items ?? [], path: data?.path ?? path, service: data?.service ?? service }; } /** ClickUp GET /task/{taskId} — list.id for resolving list-scoped fields when only task id is known. */ export async function fetchClickupTask( request: ApiRequestFunction, connectionId: string, taskId: string ): Promise> { const data = await request({ url: `/api/clickup/${connectionId}/tasks/${encodeURIComponent(taskId)}`, method: 'get', }); return data && typeof data === 'object' ? (data as Record) : {}; } /** ClickUp list metadata (statuses, etc.) — GET /api/clickup/{connectionId}/lists/{listId}. */ export async function fetchClickupList( request: ApiRequestFunction, connectionId: string, listId: string ): Promise> { const data = await request({ url: `/api/clickup/${connectionId}/lists/${listId}`, method: 'get', }); return data && typeof data === 'object' ? (data as Record) : {}; } /** ClickUp workspace/team (members for assignees) — GET /api/clickup/{connectionId}/teams/{teamId}. */ export async function fetchClickupTeam( request: ApiRequestFunction, connectionId: string, teamId: string ): Promise> { const data = await request({ url: `/api/clickup/${connectionId}/teams/${teamId}`, method: 'get', }); return data && typeof data === 'object' ? (data as Record) : {}; } /** ClickUp list custom fields (GET /api/clickup/{connectionId}/lists/{listId}/fields). */ export async function fetchClickupListFields( request: ApiRequestFunction, connectionId: string, listId: string ): Promise<{ fields?: unknown[] } & Record> { const data = await request({ url: `/api/clickup/${connectionId}/lists/${listId}/fields`, method: 'get', }); return (data && typeof data === 'object' ? data : {}) as { fields?: unknown[] } & Record; } /** ClickUp GET /list/{id}/task page (tasks in a list for relationship dropdowns). */ export interface ClickupListTaskItem { id?: string; name?: string; } export async function fetchClickupListTasks( request: ApiRequestFunction, connectionId: string, listId: string, options?: { page?: number; includeClosed?: boolean } ): Promise< { tasks?: ClickupListTaskItem[]; last_page?: boolean } & Record > { const data = await request({ url: `/api/clickup/${connectionId}/lists/${listId}/tasks`, method: 'get', params: { page: options?.page ?? 0, include_closed: options?.includeClosed ?? false, }, }); return (data && typeof data === 'object' ? data : {}) as { tasks?: ClickupListTaskItem[]; last_page?: boolean; } & Record; } /** Paginated tasks in a list — for ClickUp relationship dropdowns and input.form „ClickUp-Aufgabe“. */ export async function loadClickupListTasksForDropdown( request: ApiRequestFunction, connectionId: string, listId: string ): Promise> { const acc: Array<{ id: string; name: string }> = []; const seen = new Set(); const maxPages = 12; const pageSizeHint = 100; for (let page = 0; page < maxPages; page++) { const data = await fetchClickupListTasks(request, connectionId, listId, { page, includeClosed: false, }); if (data && typeof data === 'object' && 'error' in data && (data as { error?: unknown }).error) { const err = (data as { error?: unknown }).error; const body = (data as { body?: string }).body; throw new Error( typeof err === 'string' ? err + (body ? `: ${body.slice(0, 200)}` : '') : 'ClickUp API error' ); } const tasks = Array.isArray(data.tasks) ? data.tasks : []; for (const t of tasks) { const id = t?.id != null ? String(t.id) : ''; if (!id || seen.has(id)) continue; seen.add(id); acc.push({ id, name: String(t.name ?? id) }); } const rawLast = (data as Record).last_page; const last = rawLast === true || rawLast === 'true' || tasks.length === 0 || tasks.length < pageSizeHint; if (last) break; } acc.sort((a, b) => a.name.localeCompare(b.name, 'de')); return acc; }