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).
356 lines
9.2 KiB
TypeScript
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 };
|
|
}
|