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
JavaScript
"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;