346 lines
9.1 KiB
JavaScript
346 lines
9.1 KiB
JavaScript
const fs = require("fs").promises;
|
|
const path = require("path");
|
|
|
|
/**
|
|
* ScheduleService - Manages scheduled operations with JSON persistence
|
|
* Handles CRUD operations for schedule management in the TUI
|
|
*/
|
|
class ScheduleService {
|
|
constructor() {
|
|
this.schedulesFile = path.join(process.cwd(), "schedules.json");
|
|
}
|
|
|
|
/**
|
|
* Load schedules from JSON file
|
|
* @returns {Promise<Array>} Array of schedule objects
|
|
*/
|
|
async loadSchedules() {
|
|
try {
|
|
const data = await fs.readFile(this.schedulesFile, "utf8");
|
|
const schedules = JSON.parse(data);
|
|
|
|
// Ensure all schedules have required properties and convert date strings back to Date objects
|
|
return schedules.map((schedule) => ({
|
|
...schedule,
|
|
scheduledTime: new Date(schedule.scheduledTime),
|
|
createdAt: new Date(schedule.createdAt),
|
|
lastExecuted: schedule.lastExecuted
|
|
? new Date(schedule.lastExecuted)
|
|
: null,
|
|
nextExecution: schedule.nextExecution
|
|
? new Date(schedule.nextExecution)
|
|
: null,
|
|
}));
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") {
|
|
// File doesn't exist, return empty array
|
|
return [];
|
|
}
|
|
throw new Error(`Failed to load schedules: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save schedules to JSON file
|
|
* @param {Array} schedules - Array of schedule objects
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async saveSchedules(schedules) {
|
|
try {
|
|
// Convert Date objects to ISO strings for JSON serialization
|
|
const serializedSchedules = schedules.map((schedule) => ({
|
|
...schedule,
|
|
scheduledTime: schedule.scheduledTime.toISOString(),
|
|
createdAt: schedule.createdAt.toISOString(),
|
|
lastExecuted: schedule.lastExecuted
|
|
? schedule.lastExecuted.toISOString()
|
|
: null,
|
|
nextExecution: schedule.nextExecution
|
|
? schedule.nextExecution.toISOString()
|
|
: null,
|
|
}));
|
|
|
|
await fs.writeFile(
|
|
this.schedulesFile,
|
|
JSON.stringify(serializedSchedules, null, 2),
|
|
"utf8"
|
|
);
|
|
} catch (error) {
|
|
throw new Error(`Failed to save schedules: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a new schedule
|
|
* @param {Object} schedule - Schedule object to add
|
|
* @returns {Promise<Object>} The added schedule with generated ID
|
|
*/
|
|
async addSchedule(schedule) {
|
|
const validationError = this.validateSchedule(schedule);
|
|
if (validationError) {
|
|
throw new Error(`Invalid schedule: ${validationError}`);
|
|
}
|
|
|
|
const schedules = await this.loadSchedules();
|
|
|
|
// Generate unique ID
|
|
const id = this._generateId(schedules);
|
|
|
|
// Create new schedule with defaults
|
|
const newSchedule = {
|
|
id,
|
|
operationType: schedule.operationType,
|
|
scheduledTime: new Date(schedule.scheduledTime),
|
|
recurrence: schedule.recurrence || "once",
|
|
enabled: schedule.enabled !== undefined ? schedule.enabled : true,
|
|
config: schedule.config || {},
|
|
status: "pending",
|
|
createdAt: new Date(),
|
|
lastExecuted: null,
|
|
nextExecution: this._calculateNextExecution(
|
|
new Date(schedule.scheduledTime),
|
|
schedule.recurrence || "once"
|
|
),
|
|
};
|
|
|
|
schedules.push(newSchedule);
|
|
await this.saveSchedules(schedules);
|
|
|
|
return newSchedule;
|
|
}
|
|
|
|
/**
|
|
* Update an existing schedule
|
|
* @param {string} id - Schedule ID to update
|
|
* @param {Object} updates - Updates to apply
|
|
* @returns {Promise<Object>} The updated schedule
|
|
*/
|
|
async updateSchedule(id, updates) {
|
|
const schedules = await this.loadSchedules();
|
|
const scheduleIndex = schedules.findIndex((s) => s.id === id);
|
|
|
|
if (scheduleIndex === -1) {
|
|
throw new Error(`Schedule with ID ${id} not found`);
|
|
}
|
|
|
|
// Merge updates with existing schedule
|
|
const updatedSchedule = {
|
|
...schedules[scheduleIndex],
|
|
...updates,
|
|
};
|
|
|
|
// Validate the updated schedule
|
|
const validationError = this.validateSchedule(updatedSchedule);
|
|
if (validationError) {
|
|
throw new Error(`Invalid schedule update: ${validationError}`);
|
|
}
|
|
|
|
// Ensure dates are Date objects
|
|
if (updates.scheduledTime) {
|
|
updatedSchedule.scheduledTime = new Date(updates.scheduledTime);
|
|
updatedSchedule.nextExecution = this._calculateNextExecution(
|
|
updatedSchedule.scheduledTime,
|
|
updatedSchedule.recurrence
|
|
);
|
|
}
|
|
|
|
schedules[scheduleIndex] = updatedSchedule;
|
|
await this.saveSchedules(schedules);
|
|
|
|
return updatedSchedule;
|
|
}
|
|
|
|
/**
|
|
* Delete a schedule
|
|
* @param {string} id - Schedule ID to delete
|
|
* @returns {Promise<boolean>} True if deleted, false if not found
|
|
*/
|
|
async deleteSchedule(id) {
|
|
const schedules = await this.loadSchedules();
|
|
const initialLength = schedules.length;
|
|
const filteredSchedules = schedules.filter((s) => s.id !== id);
|
|
|
|
if (filteredSchedules.length === initialLength) {
|
|
return false; // Schedule not found
|
|
}
|
|
|
|
await this.saveSchedules(filteredSchedules);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate schedule data
|
|
* @param {Object} schedule - Schedule object to validate
|
|
* @returns {string|null} Error message if invalid, null if valid
|
|
*/
|
|
validateSchedule(schedule) {
|
|
if (!schedule) {
|
|
return "Schedule object is required";
|
|
}
|
|
|
|
// Validate operation type
|
|
if (!schedule.operationType) {
|
|
return "Operation type is required";
|
|
}
|
|
if (!["update", "rollback"].includes(schedule.operationType)) {
|
|
return 'Operation type must be "update" or "rollback"';
|
|
}
|
|
|
|
// Validate scheduled time
|
|
if (!schedule.scheduledTime) {
|
|
return "Scheduled time is required";
|
|
}
|
|
|
|
const scheduledTime = new Date(schedule.scheduledTime);
|
|
if (isNaN(scheduledTime.getTime())) {
|
|
return "Scheduled time must be a valid date";
|
|
}
|
|
|
|
// Check if scheduled time is in the future (for new schedules)
|
|
if (!schedule.id && scheduledTime <= new Date()) {
|
|
return "Scheduled time must be in the future";
|
|
}
|
|
|
|
// Validate recurrence
|
|
if (
|
|
schedule.recurrence &&
|
|
!["once", "daily", "weekly", "monthly"].includes(schedule.recurrence)
|
|
) {
|
|
return "Recurrence must be one of: once, daily, weekly, monthly";
|
|
}
|
|
|
|
// Validate status
|
|
if (
|
|
schedule.status &&
|
|
!["pending", "completed", "failed", "cancelled"].includes(schedule.status)
|
|
) {
|
|
return "Status must be one of: pending, completed, failed, cancelled";
|
|
}
|
|
|
|
// Validate enabled flag
|
|
if (
|
|
schedule.enabled !== undefined &&
|
|
typeof schedule.enabled !== "boolean"
|
|
) {
|
|
return "Enabled must be a boolean value";
|
|
}
|
|
|
|
// Validate config object
|
|
if (schedule.config && typeof schedule.config !== "object") {
|
|
return "Config must be an object";
|
|
}
|
|
|
|
return null; // Valid
|
|
}
|
|
|
|
/**
|
|
* Generate unique ID for new schedule
|
|
* @param {Array} existingSchedules - Array of existing schedules
|
|
* @returns {string} Unique ID
|
|
* @private
|
|
*/
|
|
_generateId(existingSchedules) {
|
|
const timestamp = Date.now();
|
|
const random = Math.random().toString(36).substr(2, 9);
|
|
let id = `schedule_${timestamp}_${random}`;
|
|
|
|
// Ensure uniqueness (very unlikely collision, but safety check)
|
|
while (existingSchedules.some((s) => s.id === id)) {
|
|
const newRandom = Math.random().toString(36).substr(2, 9);
|
|
id = `schedule_${timestamp}_${newRandom}`;
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Calculate next execution time based on recurrence
|
|
* @param {Date} scheduledTime - Original scheduled time
|
|
* @param {string} recurrence - Recurrence pattern
|
|
* @returns {Date|null} Next execution time or null for 'once'
|
|
* @private
|
|
*/
|
|
_calculateNextExecution(scheduledTime, recurrence) {
|
|
if (recurrence === "once") {
|
|
return null;
|
|
}
|
|
|
|
const nextExecution = new Date(scheduledTime);
|
|
|
|
switch (recurrence) {
|
|
case "daily":
|
|
nextExecution.setDate(nextExecution.getDate() + 1);
|
|
break;
|
|
case "weekly":
|
|
nextExecution.setDate(nextExecution.getDate() + 7);
|
|
break;
|
|
case "monthly":
|
|
nextExecution.setMonth(nextExecution.getMonth() + 1);
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
return nextExecution;
|
|
}
|
|
|
|
/**
|
|
* Get schedules by status
|
|
* @param {string} status - Status to filter by
|
|
* @returns {Promise<Array>} Filtered schedules
|
|
*/
|
|
async getSchedulesByStatus(status) {
|
|
const schedules = await this.loadSchedules();
|
|
return schedules.filter((schedule) => schedule.status === status);
|
|
}
|
|
|
|
/**
|
|
* Get schedules by operation type
|
|
* @param {string} operationType - Operation type to filter by
|
|
* @returns {Promise<Array>} Filtered schedules
|
|
*/
|
|
async getSchedulesByOperationType(operationType) {
|
|
const schedules = await this.loadSchedules();
|
|
return schedules.filter(
|
|
(schedule) => schedule.operationType === operationType
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get enabled schedules
|
|
* @returns {Promise<Array>} Enabled schedules
|
|
*/
|
|
async getEnabledSchedules() {
|
|
const schedules = await this.loadSchedules();
|
|
return schedules.filter((schedule) => schedule.enabled);
|
|
}
|
|
|
|
/**
|
|
* Mark schedule as completed
|
|
* @param {string} id - Schedule ID
|
|
* @returns {Promise<Object>} Updated schedule
|
|
*/
|
|
async markScheduleCompleted(id) {
|
|
return await this.updateSchedule(id, {
|
|
status: "completed",
|
|
lastExecuted: new Date(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Mark schedule as failed
|
|
* @param {string} id - Schedule ID
|
|
* @param {string} errorMessage - Error message
|
|
* @returns {Promise<Object>} Updated schedule
|
|
*/
|
|
async markScheduleFailed(id, errorMessage) {
|
|
return await this.updateSchedule(id, {
|
|
status: "failed",
|
|
lastExecuted: new Date(),
|
|
errorMessage: errorMessage,
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = ScheduleService;
|