UNPKG

powr-sdk-api

Version:

Shared API core library for PowrStack projects. Zero dependencies - works with Express, Next.js API routes, and other frameworks. All features are optional and install only what you need.

757 lines (707 loc) 23.9 kB
"use strict"; const { getDb } = require("../services/mongo"); const { ObjectId } = require("mongodb"); class TasksManager { // Create a new scheduled task async createTask(taskData) { try { const db = await getDb(); const task = { name: taskData.name, description: taskData.description, type: taskData.type || 'task', // 'task' or 'workflow' userId: taskData.userId, projectId: taskData.projectId, isActive: taskData.isActive !== false, // Default to true scheduledFor: taskData.scheduledFor, // null, timestamp, or cron expression nextRun: null, // Will be calculated based on scheduling type createdAt: new Date(), updatedAt: new Date() }; // Handle single task vs workflow if (task.type === 'task') { // Single task - backward compatible task.toolId = taskData.toolId; task.actionId = taskData.actionId; task.params = taskData.params; } else if (task.type === 'workflow') { // Workflow - new structure task.steps = taskData.steps || []; task.trigger = taskData.trigger || 'manual'; task.schedule = taskData.schedule || null; } // Calculate nextRun based on scheduling type if (!task.scheduledFor) { // Immediate execution - run on next Atlas trigger task.nextRun = new Date(); } else if (this.isTimestamp(task.scheduledFor)) { // One-time execution - run at the specified time task.nextRun = new Date(task.scheduledFor); } else if (this.isCronExpression(task.scheduledFor)) { // Recurring execution - calculate next run from cron task.nextRun = this.getNextRunTime(task.scheduledFor); } await db.collection("tasks").insertOne(task); console.log(`✅ Created scheduled task: ${task.name} (${task.id})`); console.log(`📅 Next run: ${task.nextRun}`); return { success: true, task }; } catch (error) { console.error("❌ Failed to create scheduled task:", error); return { success: false, message: error.message }; } } // Get all tasks for a user (filtered by projectId and userId) async getUserTasks(userId, projectId, isAdmin = false) { try { const db = await getDb(); // Build query based on user role let query = {}; if (isAdmin) { // Admin sees all tasks in the project query.projectId = projectId; } else { // Regular users see only their tasks in the project query.userId = userId; query.projectId = projectId; } const tasks = await db.collection("tasks").find(query).sort({ createdAt: -1 }).toArray(); // For workflows, get the last execution time for (const task of tasks) { if (task.type === 'workflow') { const lastExecution = await db.collection("workflow_executions").findOne({ workflowId: task._id }, { sort: { executedAt: -1 } }); task.lastRun = (lastExecution === null || lastExecution === void 0 ? void 0 : lastExecution.executedAt) || null; } } return { success: true, tasks }; } catch (error) { console.error("❌ Failed to get user tasks:", error); return { success: false, message: error.message }; } } // Get a specific task (with project and user validation) async getTask(taskId, userId, projectId, isAdmin = false) { try { const db = await getDb(); // Build query based on user role let query = { _id: new ObjectId(taskId) }; if (isAdmin) { // Admin can access any task in the project query.projectId = projectId; } else { // Regular users can only access their own tasks query.userId = userId; query.projectId = projectId; } const task = await db.collection("tasks").findOne(query); if (!task) { return { success: false, message: "Task not found" }; } return { success: true, task }; } catch (error) { console.error("❌ Failed to get task:", error); return { success: false, message: error.message }; } } // Update a task (with project and user validation) async updateTask(taskId, userId, projectId, updateData, isAdmin = false) { try { const db = await getDb(); // Build query based on user role let query = { _id: new ObjectId(taskId) }; if (isAdmin) { // Admin can update any task in the project query.projectId = projectId; } else { // Regular users can only update their own tasks query.userId = userId; query.projectId = projectId; } const task = await db.collection("tasks").findOne(query); if (!task) { return { success: false, message: "Task not found" }; } // Update the task const updateResult = await db.collection("tasks").updateOne(query, { $set: { ...updateData, updatedAt: new Date(), nextRun: updateData.scheduledFor ? this.getNextRunTime(updateData.scheduledFor) : task.nextRun } }); if (updateResult.modifiedCount === 0) { return { success: false, message: "Failed to update task" }; } console.log(`✅ Updated scheduled task: ${taskId}`); return { success: true }; } catch (error) { console.error("❌ Failed to update scheduled task:", error); return { success: false, message: error.message }; } } // Delete a task (with project and user validation) async deleteTask(taskId, userId, projectId, isAdmin = false) { try { const db = await getDb(); // Build query based on user role let query = { _id: new ObjectId(taskId) }; if (isAdmin) { // Admin can delete any task in the project query.projectId = projectId; } else { // Regular users can only delete their own tasks query.userId = userId; query.projectId = projectId; } // Get task to check if it exists const task = await db.collection("tasks").findOne(query); if (!task) { return { success: false, message: "Task not found" }; } // Remove from database await db.collection("tasks").deleteOne(query); // No need to unschedule - MongoDB Atlas will handle this automatically console.log(`✅ Deleted scheduled task: ${task.name} (${taskId})`); return { success: true }; } catch (error) { console.error("❌ Failed to delete scheduled task:", error); return { success: false, message: error.message }; } } // Toggle task active status (with project and user validation) async toggleTask(taskId, userId, projectId, isActive, isAdmin = false) { try { const db = await getDb(); // Build query based on user role let query = { _id: new ObjectId(taskId) }; if (isAdmin) { // Admin can toggle any task in the project query.projectId = projectId; } else { // Regular users can only toggle their own tasks query.userId = userId; query.projectId = projectId; } const task = await db.collection("tasks").findOne(query); if (!task) { return { success: false, message: "Task not found" }; } // Update active status await db.collection("tasks").updateOne(query, { $set: { isActive: isActive, updatedAt: new Date() } }); console.log(`${isActive ? '✅' : '❌'} Task ${taskId} ${isActive ? 'activated' : 'deactivated'}`); console.log(`📅 MongoDB Atlas Scheduled Trigger will ${isActive ? 'start' : 'stop'} executing this task.`); return { success: true }; } catch (error) { console.error("❌ Failed to toggle task:", error); return { success: false, message: error.message }; } } // Execute a task manually (with project and user validation) async executeTask(taskId, userId, projectId, isAdmin = false) { try { const db = await getDb(); // Build query based on user role let query = { _id: new ObjectId(taskId) }; if (isAdmin) { // Admin can execute any task in the project query.projectId = projectId; } else { // Regular users can only execute their own tasks query.userId = userId; query.projectId = projectId; } const task = await db.collection("tasks").findOne(query); if (!task) { return { success: false, message: "Task not found" }; } console.log(`🚀 Manually executing task: ${task.name} (${taskId})`); // Execute the task const result = await this.executeTaskAction(task); return { success: true, result }; } catch (error) { console.error("❌ Failed to execute task:", error); return { success: false, message: error.message }; } } // Execute scheduled tasks (called by MongoDB Atlas Scheduled Trigger) async executeScheduledTasks(options = {}) { const { projectId } = options; try { const db = await getDb(); const query = { projectId: projectId, isActive: true, nextRun: { $lte: new Date() } // Only tasks where nextRun <= current time }; const tasks = await db.collection("tasks").find(query).toArray(); for (const task of tasks) { try { console.log(`⏰ Executing: ${task.name} (${task._id}) for project ${task.projectId}`); // Execute the task directly (we already have the task data) const result = await this.executeTaskAction(task); // Handle task completion and next run time in single update const updateData = {}; if (!task.scheduledFor || this.isTimestamp(task.scheduledFor)) { // For immediate and one-time tasks, deactivate after execution updateData.isActive = false; console.log(`✅ Task ${task._id} completed and deactivated`); } else if (this.isCronExpression(task.scheduledFor)) { // For recurring tasks, calculate next run time updateData.nextRun = this.getNextRunTime(task.scheduledFor); console.log(`📅 Updated next run time for task ${task._id}: ${updateData.nextRun}`); } await db.collection("tasks").updateOne({ _id: task._id }, { $set: updateData }); console.log(`✅ Task ${task._id} completed:`, result.success ? 'SUCCESS' : 'FAILED'); } catch (error) { console.error(`❌ Task ${task._id} failed:`, error.message); } } return { success: true, tasksExecuted: tasks.length, projectId: projectId }; } catch (error) { console.error("❌ Failed to execute scheduled tasks:", error); return { success: false, error: error.message }; } } // Execute the actual task action async executeTaskAction(task) { try { if (task.type === 'workflow') { // Execute workflow return await this.executeWorkflow(task); } else { // Execute single task (backward compatible) const toolsManager = require('./tools'); const result = await toolsManager.executeToolAction(task); await this.logTaskExecution(task, result); return result; } } catch (error) { console.error(`❌ Task execution failed for ${task._id}:`, error); return { success: false, message: error.message }; } } // Execute workflow with multiple steps and conditions async executeWorkflow(workflow) { try { console.log(`🔄 Executing workflow: ${workflow.name}`); const results = []; let workflowSuccess = true; for (const step of workflow.steps) { try { console.log(`📋 Executing step: ${step.type} - ${step.toolId}/${step.actionId}`); if (step.type === 'condition') { // Execute condition step const conditionResult = await this.executeConditionStep(step, workflow.userId); results.push({ stepId: step.id, type: 'condition', success: conditionResult.success, result: conditionResult, conditionMet: conditionResult.conditionMet }); // Store condition result for subsequent steps step.result = conditionResult; } else if (step.type === 'action') { // Check if action should execute based on conditions if (this.shouldExecuteAction(step, results)) { const actionResult = await this.executeActionStep(step, workflow.userId); results.push({ stepId: step.id, type: 'action', success: actionResult.success, result: actionResult }); if (!actionResult.success) { workflowSuccess = false; } } else { console.log(`⏭️ Skipping action step ${step.id} - condition not met`); results.push({ stepId: step.id, type: 'action', success: true, result: { message: 'Step skipped - condition not met' }, skipped: true }); } } } catch (error) { console.error(`❌ Step execution failed: ${step.id}`, error); results.push({ stepId: step.id, type: step.type, success: false, error: error.message }); workflowSuccess = false; } } // Log workflow execution await this.logWorkflowExecution(workflow, results); // For one-time workflows, deactivate after execution if (workflow.schedule && !this.isCronExpression(workflow.schedule)) { try { const db = await getDb(); await db.collection("tasks").updateOne({ _id: workflow._id }, { $set: { isActive: false } }); console.log(`🔄 Deactivated one-time workflow: ${workflow.name}`); } catch (error) { console.error("❌ Failed to deactivate workflow:", error); } } // Determine overall success - consider it successful if at least one action executed successfully const executedActions = results.filter(r => r.type === 'action' && !r.skipped); const successfulActions = executedActions.filter(r => r.success); const overallSuccess = executedActions.length === 0 || successfulActions.length > 0; return { success: overallSuccess, workflowName: workflow.name, stepsExecuted: results.length, results: results, actionsExecuted: executedActions.length, actionsSuccessful: successfulActions.length }; } catch (error) { console.error(`❌ Workflow execution failed: ${workflow.name}`, error); return { success: false, message: error.message }; } } // Execute a condition step (e.g., weather check) async executeConditionStep(step, userId) { try { const toolsManager = require('./tools'); const result = await toolsManager.executeToolAction({ userId: userId, toolId: step.toolId, actionId: step.actionId, params: step.params }); if (result.success) { var _result$data, _result$data2, _step$params; // Extract condition result (e.g., weather condition) const conditionValue = ((_result$data = result.data) === null || _result$data === void 0 ? void 0 : _result$data.currentWeather) || ((_result$data2 = result.data) === null || _result$data2 === void 0 ? void 0 : _result$data2.condition) || 'unknown'; const expectedValue = step.expectedResult || ((_step$params = step.params) === null || _step$params === void 0 ? void 0 : _step$params.condition) || 'rain'; const conditionMet = conditionValue.toLowerCase() === expectedValue.toLowerCase(); return { success: true, conditionMet: conditionMet, actualValue: conditionValue, expectedValue: expectedValue, data: result.data }; } else { return { success: false, conditionMet: false, error: result.message }; } } catch (error) { return { success: false, conditionMet: false, error: error.message }; } } // Execute an action step (e.g., send email) async executeActionStep(step, userId) { try { const toolsManager = require('./tools'); const result = await toolsManager.executeToolAction({ userId: userId, toolId: step.toolId, actionId: step.actionId, params: step.params }); return result; } catch (error) { return { success: false, message: error.message }; } } // Check if action should execute based on conditions shouldExecuteAction(step, previousResults) { if (!step.condition) { return true; // No condition, always execute } // Parse condition like "step1.result === 'rain'" or "step1.result !== 'rain'" const condition = step.condition; // Extract step ID and condition const stepMatch = condition.match(/step(\d+)\.result\s*(===|!==)\s*['"]([^'"]+)['"]/); if (stepMatch) { const stepId = stepMatch[1]; const operator = stepMatch[2]; const expectedValue = stepMatch[3]; const stepResult = previousResults.find(r => r.stepId === stepId); if (stepResult && stepResult.result) { var _stepResult$result$da; // Get actual value from the result structure const actualValue = stepResult.result.actualValue || ((_stepResult$result$da = stepResult.result.data) === null || _stepResult$result$da === void 0 ? void 0 : _stepResult$result$da.currentWeather) || stepResult.result.condition || 'unknown'; console.log(`🔍 Condition check: ${actualValue} ${operator} ${expectedValue}`); if (operator === '===') { return actualValue.toLowerCase() === expectedValue.toLowerCase(); } else if (operator === '!==') { return actualValue.toLowerCase() !== expectedValue.toLowerCase(); } } } // Fallback: check for simple boolean conditions if (condition.includes('step1') && condition.includes('=== true')) { const step1Result = previousResults.find(r => r.stepId === 'step1'); return step1Result && step1Result.conditionMet === true; } console.log(`⚠️ Could not parse condition: ${condition}, defaulting to execute`); return true; // Default to execute if condition parsing fails } // Log task execution async logTaskExecution(task, result) { try { const db = await getDb(); await db.collection("task_executions").insertOne({ taskId: task._id || task.id, userId: task.userId, type: task.type || 'task', toolId: task.toolId, actionId: task.actionId, params: task.params, result: result, executedAt: new Date() }); } catch (error) { console.error("❌ Failed to log task execution:", error); } } // Log workflow execution async logWorkflowExecution(workflow, results) { try { const db = await getDb(); // Calculate overall success const executedActions = results.filter(r => r.type === 'action' && !r.skipped); const successfulActions = executedActions.filter(r => r.success); const overallSuccess = executedActions.length === 0 || successfulActions.length > 0; await db.collection("workflow_executions").insertOne({ workflowId: workflow._id, workflowName: workflow.name, userId: workflow.userId, projectId: workflow.projectId, results: results, success: overallSuccess, actionsExecuted: executedActions.length, actionsSuccessful: successfulActions.length, executedAt: new Date() }); } catch (error) { console.error("❌ Failed to log workflow execution:", error); } } // Get execution history for a task async getTaskExecutions(taskId, userId, projectId, isAdmin = false) { try { const db = await getDb(); // First verify the task exists and user has access let taskQuery = { _id: new ObjectId(taskId) }; if (isAdmin) { taskQuery.projectId = projectId; } else { taskQuery.userId = userId; taskQuery.projectId = projectId; } const task = await db.collection("tasks").findOne(taskQuery); if (!task) { return { success: false, message: "Task not found" }; } // Get execution history based on task type let executions = []; if (task.type === 'workflow') { // Get workflow executions executions = await db.collection("workflow_executions").find({ workflowId: new ObjectId(taskId) }).sort({ executedAt: -1 }).limit(50) // Limit to last 50 executions .toArray(); } else { // Get task executions executions = await db.collection("task_executions").find({ taskId: new ObjectId(taskId) }).sort({ executedAt: -1 }).limit(50) // Limit to last 50 executions .toArray(); } return { success: true, executions }; } catch (error) { console.error("❌ Failed to get task executions:", error); return { success: false, message: error.message }; } } // Check if a string is a valid timestamp isTimestamp(value) { try { const date = new Date(value); return !isNaN(date.getTime()) && value !== date.toISOString().slice(0, 10); // Not just a date } catch (error) { return false; } } // Check if a string is a cron expression isCronExpression(value) { if (!value || typeof value !== 'string') return false; // Basic cron pattern: 5 or 6 fields separated by spaces const cronPattern = /^(\*|\d+|\d+-\d+|\d+\/\d+)(\s+(\*|\d+|\d+-\d+|\d+\/\d+)){4,5}$/; return cronPattern.test(value.trim()); } // Check if a string is a cron expression isCronExpression(value) { try { const parser = require('cron-parser'); parser.parseExpression(value); return true; } catch (error) { return false; } } // Get next run time for cron expression getNextRunTime(cronExpression) { try { const parser = require('cron-parser'); const interval = parser.parseExpression(cronExpression); return interval.next().toDate(); } catch (error) { console.error("❌ Failed to parse cron expression:", error); return null; } } } // Create and export singleton instance const tasksManager = new TasksManager(); module.exports = tasksManager;