ui-nyla/src/api/automation2Api.ts
ValueOn AG 77e7eba711 BREAKING CHANGE
API and persisted records use PowerOnModel system fields:
- sysCreatedAt, sysCreatedBy, sysModifiedAt, sysModifiedBy
Removed legacy JSON/DB field names:
- _createdAt, _createdBy, _modifiedAt, _modifiedBy
Frontend (frontend_nyla) and gateway call sites were updated accordingly.
Database:
- Bootstrap runs idempotent backfill (_migrateSystemFieldColumns) from old
  underscore columns and selected business duplicates into sys* where sys* IS NULL.
- Re-run app bootstrap against each PostgreSQL database after deploy.
- Optional: DROP INDEX IF EXISTS "idx_invitation_createdby" if an old index remains;
  new index: idx_invitation_syscreatedby on Invitation(sysCreatedBy).
Tests:
- RBAC integration tests aligned with current GROUP mandate filter and UserMandate-based
  UserConnection GROUP clause; buildRbacWhereClause(..., mandateId=...) must be passed
  explicitly (same as production request context).
2026-03-28 18:13:18 +01:00

356 lines
9.2 KiB
TypeScript

/**
* 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;
executor: string;
meta?: {
icon?: string;
color?: string;
method?: string;
action?: string;
};
}
export interface NodeTypeCategory {
id: string;
label: Record<string, string> | string;
}
export interface NodeTypesResponse {
nodeTypes: NodeType[];
categories: NodeTypeCategory[];
}
export interface Automation2GraphNode {
id: string;
type: string;
parameters?: Record<string, unknown>;
}
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<string, unknown>;
error?: string;
stopped?: boolean;
failedNode?: string;
paused?: boolean;
taskId?: string;
runId?: string;
nodeId?: string;
}
export interface Automation2Workflow {
id: string;
label: string;
graph: Automation2Graph;
active?: boolean;
/** 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<any>) => Promise<any>;
/**
* 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<NodeTypesResponse> {
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 async function executeGraph(
request: ApiRequestFunction,
instanceId: string,
graph: Automation2Graph,
workflowId?: string
): Promise<ExecuteGraphResponse> {
console.log(
`${LOG} executeGraph request: instanceId=${instanceId} workflowId=${workflowId} nodes=${graph.nodes.length} connections=${graph.connections.length}`,
{ nodes: graph.nodes, connections: graph.connections }
);
const start = performance.now();
try {
const result = await request({
url: `/api/automation2/${instanceId}/execute`,
method: 'post',
data: { graph, workflowId },
});
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
): Promise<Automation2Workflow[]> {
const data = await request({
url: `/api/automation2/${instanceId}/workflows`,
method: 'get',
});
return data?.workflows ?? [];
}
export async function fetchWorkflow(
request: ApiRequestFunction,
instanceId: string,
workflowId: string
): Promise<Automation2Workflow> {
return await request({
url: `/api/automation2/${instanceId}/workflows/${workflowId}`,
method: 'get',
});
}
export async function createWorkflow(
request: ApiRequestFunction,
instanceId: string,
body: { label: string; graph: Automation2Graph }
): Promise<Automation2Workflow> {
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 }
): Promise<Automation2Workflow> {
return await request({
url: `/api/automation2/${instanceId}/workflows/${workflowId}`,
method: 'put',
data: body,
});
}
export async function deleteWorkflow(
request: ApiRequestFunction,
instanceId: string,
workflowId: string
): Promise<void> {
await request({
url: `/api/automation2/${instanceId}/workflows/${workflowId}`,
method: 'delete',
});
}
export interface Automation2Run {
id: string;
workflowId: string;
status: string;
nodeOutputs?: Record<string, unknown>;
currentNodeId?: string;
}
export async function fetchWorkflowRuns(
request: ApiRequestFunction,
instanceId: string,
workflowId: string
): Promise<Automation2Run[]> {
const data = await request({
url: `/api/automation2/${instanceId}/workflows/${workflowId}/runs`,
method: 'get',
});
return data?.runs ?? [];
}
// -------------------------------------------------------------------------
// Tasks
// -------------------------------------------------------------------------
export interface Automation2Task {
id: string;
runId: string;
workflowId: string;
nodeId: string;
nodeType: string;
config: Record<string, unknown>;
status: string;
result?: Record<string, unknown>;
/** 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<Automation2Task[]> {
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<string, unknown>
): Promise<ExecuteGraphResponse> {
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<UserConnection[]> {
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<ConnectionService[]> {
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<string, unknown>;
}
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 };
}