Just a whole lot of crap
This commit is contained in:
345
src/services/scheduleManagement.js
Normal file
345
src/services/scheduleManagement.js
Normal file
@@ -0,0 +1,345 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user