Files
PriceUpdaterAppv2/src/services/scheduleManagement.js

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;