import type { Knex } from 'knex'; import { v4 as uuidv4 } from 'uuid'; import WorkflowTaskModel, { WorkflowTaskStatus, publishWorkflowTaskSearchEvent, } from '../workflow/persistence/workflowTaskModel'; import { TaskCreationParams } from '../workflow/persistence/taskInboxInterfaces'; /** * Service for managing workflow tasks in the Task Inbox system */ export class TaskInboxService { /** * Create a new task in the Task Inbox * * @param knex Knex instance * @param tenant Tenant ID * @param executionId Workflow execution ID * @param params Task creation parameters * @param userId User ID of the creator * @returns The created task ID */ async createTask( knex: Knex, tenant: string, executionId: string, params: TaskCreationParams, userId?: string ): Promise { try { // taskId is generated by WorkflowTaskModel.createTask let resolvedDefinitionType: 'tenant' | 'system'; let tenantTaskDefinitionIdForTask: string | null = null; let systemTaskDefinitionTaskTypeForTask: string | null = null; // 1. Attempt to find tenantTaskDef const tenantTaskDef = await knex('workflow_task_definitions') .where({ form_id: params.taskType, tenant: tenant, // Current tenant }) .first(); if (tenantTaskDef) { tenantTaskDefinitionIdForTask = tenantTaskDef.task_definition_id; resolvedDefinitionType = 'tenant'; } else { // 2. If tenantTaskDef not found, attempt to find systemTaskDef const systemTaskDef = await knex('system_workflow_task_definitions') .where({ task_type: params.taskType, }) .first(); if (systemTaskDef) { systemTaskDefinitionTaskTypeForTask = systemTaskDef.task_type; // For system tasks, task_type is the ID resolvedDefinitionType = 'system'; } else { // If neither tenant nor system definition is found, throw an error. // Task definitions must exist before tasks can be created. throw new Error(`Task definition with taskType '${params.taskType}' not found for tenant '${tenant}' or as a system definition.`); } } let assignedRoles: string[] | undefined = undefined; if (params.assignTo?.roles) { if (typeof params.assignTo.roles === 'string') { assignedRoles = [params.assignTo.roles]; } else if (Array.isArray(params.assignTo.roles)) { assignedRoles = params.assignTo.roles; } } let assignedUsers: string[] | undefined = undefined; if (params.assignTo?.users) { if (typeof params.assignTo.users === 'string') { assignedUsers = [params.assignTo.users]; } else if (Array.isArray(params.assignTo.users)) { assignedUsers = params.assignTo.users; } } if (assignedRoles !== undefined && !Array.isArray(assignedRoles)) { assignedRoles = undefined; } if (assignedUsers !== undefined && !Array.isArray(assignedUsers)) { assignedUsers = undefined; } // Prepare the task object with the new dual FK structure. // IWorkflowTask will need to be updated to reflect these new fields. // For now, we construct the object as expected by WorkflowTaskModel.createTask after its own update. const taskPayload = { tenant, execution_id: executionId, tenant_task_definition_id: tenantTaskDefinitionIdForTask, system_task_definition_task_type: systemTaskDefinitionTaskTypeForTask, task_definition_type: resolvedDefinitionType, title: params.title, description: params.description, status: WorkflowTaskStatus.PENDING, priority: params.priority || 'medium', due_date: params.dueDate ? new Date(params.dueDate).toISOString() : undefined, context_data: params.contextData, assigned_roles: assignedRoles, assigned_users: assignedUsers, created_by: userId }; const generatedTaskId = await WorkflowTaskModel.createTask(knex, tenant, taskPayload as any); // Add task history entry - using the generated task ID from the database try { await WorkflowTaskModel.addTaskHistory(knex, tenant, { task_id: generatedTaskId, // Use the task ID returned from createTask tenant, action: 'create', from_status: undefined, to_status: WorkflowTaskStatus.PENDING, user_id: userId }); } catch (error) { console.error('Error adding task history:', error); } return generatedTaskId; // Return the generated task ID from the database } catch (error) { console.error('Error creating task:', error); throw error; } } /** * Creates a task with an inline form definition * * @param knex Knex instance * @param tenant Tenant ID * @param executionId Workflow execution ID * @param params Task and form parameters * @param userId User ID of the creator * @returns The created task ID */ async createTaskWithInlineForm( knex: Knex, tenant: string, executionId: string, params: { title: string; description?: string; priority?: string; dueDate?: string | Date; assignTo?: { roles?: string[] | string; users?: string[] | string; }; contextData?: Record; form: { jsonSchema: Record; uiSchema?: Record; defaultValues?: Record; }; formCategory?: string; }, userId?: string ): Promise { try { let assignedRoles: string[] | undefined = undefined; let assignedUsers: string[] | undefined = undefined; if (params.assignTo?.roles) { if (typeof params.assignTo.roles === 'string') { assignedRoles = [params.assignTo.roles]; } else if (Array.isArray(params.assignTo.roles)) { assignedRoles = params.assignTo.roles; } } if (params.assignTo?.users) { if (typeof params.assignTo.users === 'string') { assignedUsers = [params.assignTo.users]; } else if (Array.isArray(params.assignTo.users)) { assignedUsers = params.assignTo.users; } } // Start a transaction to ensure atomicity const taskId = await knex.transaction(async (trx: Knex.Transaction) => { // Generate a unique form ID const formId = uuidv4(); const tempTaskType = `inline_task_${formId}`; // Create the temporary form in the tenant's workflow_form_definitions table await trx('workflow_form_definitions').insert({ form_id: formId, tenant: tenant, name: `Inline Form - ${params.title}`, description: `Inline form for task: ${params.title}`, version: '1.0', status: 'active', category: params.formCategory || 'inline_forms', is_temporary: true, created_by: userId, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }); // Add the form schema await trx('workflow_form_schemas').insert({ schema_id: uuidv4(), form_id: formId, tenant: tenant, json_schema: params.form.jsonSchema, ui_schema: params.form.uiSchema || {}, default_values: params.form.defaultValues || {}, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }); // Create a task definition in the tenant's task definitions table const taskDefinitionId = uuidv4(); await trx('workflow_task_definitions').insert({ task_definition_id: taskDefinitionId, tenant: tenant, name: tempTaskType, description: `Auto-generated task definition for inline form`, form_id: formId, form_type: 'tenant', default_priority: params.priority || 'medium', created_by: userId, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }); // Generate task ID const taskId = uuidv4(); // Insert the task with references to the tenant task definition await trx('workflow_tasks').insert({ task_id: taskId, tenant: tenant, execution_id: executionId, task_definition_type: 'tenant', system_task_definition_task_type: null, tenant_task_definition_id: taskDefinitionId, title: params.title, description: params.description, status: WorkflowTaskStatus.PENDING, priority: params.priority || 'medium', due_date: params.dueDate ? new Date(params.dueDate).toISOString() : null, context_data: params.contextData ? JSON.stringify(params.contextData) : null, assigned_roles: assignedRoles ? JSON.stringify(assignedRoles) : null, assigned_users: assignedUsers ? JSON.stringify(assignedUsers) : null, created_by: userId, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }); // Add task history entry await WorkflowTaskModel.addTaskHistory(trx, tenant, { task_id: taskId, tenant: tenant, action: 'create', from_status: undefined, to_status: WorkflowTaskStatus.PENDING, user_id: userId }); return taskId; }); await publishWorkflowTaskSearchEvent('WORKFLOW_TASK_CREATED', tenant, taskId, { userId, status: WorkflowTaskStatus.PENDING, assignedUserIds: assignedUsers, changedFields: ['title', 'description', 'assigned_users', 'status'], }); return taskId; } catch (error) { console.error('Error creating task with inline form:', error); throw error; } } /** * Clean up temporary forms for a specific tenant * * @param knex Knex instance * @param tenant Tenant ID * @returns Number of forms deleted */ async cleanupTemporaryForms(knex: Knex, tenant: string): Promise { try { // First get the IDs of all temporary forms for this tenant const tempForms = await knex('workflow_form_definitions') .where({ tenant: tenant, is_temporary: true }) .select('form_id'); if (tempForms.length === 0) { return 0; } const formIds = tempForms.map((f: { form_id: string }) => f.form_id); // Delete task definitions that reference these forms await knex('workflow_task_definitions') .where({ tenant: tenant }) .whereIn('form_id', formIds) .delete(); // Delete schemas await knex('workflow_form_schemas') .where({ tenant: tenant }) .whereIn('form_id', formIds) .delete(); // Then delete the forms themselves const deletedCount = await knex('workflow_form_definitions') .where({ tenant: tenant, is_temporary: true }) .delete(); return deletedCount; } catch (error) { console.error(`Error cleaning up temporary forms for tenant ${tenant}:`, error); throw error; } } /** * Clean up temporary forms for all tenants * * @param knex Knex instance * @returns Total number of forms deleted */ async cleanupAllTemporaryForms(knex: Knex): Promise { try { // Get distinct tenants that have temporary forms const tenants = await knex('workflow_form_definitions') .where({ is_temporary: true }) .distinct('tenant') .pluck('tenant'); let totalDeleted = 0; // Clean up for each tenant for (const tenant of tenants) { const deleted = await this.cleanupTemporaryForms(knex, tenant); totalDeleted += deleted; } return totalDeleted; } catch (error) { console.error('Error cleaning up temporary forms for all tenants:', error); throw error; } } } // Singleton instance let taskInboxServiceInstance: TaskInboxService | null = null; /** * Get the task inbox service instance */ export function getTaskInboxService(): TaskInboxService { if (!taskInboxServiceInstance) { taskInboxServiceInstance = new TaskInboxService(); } return taskInboxServiceInstance; }