import CronParser from 'cron-parser'; export enum ScheduleLimits { MINUTE = 'minute', HOUR = 'hour', DAY = 'day', UNLIMITED = 'unlimited', } const SECONDS_IN_MINUTE = 60; const SECONDS_IN_HOUR = 3600; const SECONDS_IN_DAY = 86400; /** * Airflow preset schedules mapped to cron expressions. * @once is special - it means run only once (no recurring schedule). */ const AIRFLOW_PRESETS: Record = { '@once': null, // No recurring schedule - always valid '@hourly': '0 * * * *', // Every hour '@daily': '0 0 * * *', // Every day at midnight '@weekly': '0 0 * * 0', // Every week on Sunday '@monthly': '0 0 1 * *', // First day of every month '@yearly': '0 0 1 1 *', // First day of every year '@annually': '0 0 1 1 *', // Same as @yearly }; /** * Convert Airflow preset to cron expression. * Returns null for @once (no recurring schedule). * Returns original string if not an Airflow preset. */ export function convertAirflowPresetToCron(schedule: string): string | null { const preset = AIRFLOW_PRESETS[schedule.toLowerCase()]; if (preset !== undefined) { return preset; } return schedule; } export function getMinimumIntervalSeconds(scheduleLimit: string): number { switch (scheduleLimit) { case ScheduleLimits.MINUTE: return SECONDS_IN_MINUTE; case ScheduleLimits.HOUR: return SECONDS_IN_HOUR; case ScheduleLimits.DAY: return SECONDS_IN_DAY; case ScheduleLimits.UNLIMITED: default: return 0; } } export function getCronIntervalSeconds(cron: string): number { const interval = CronParser.parseExpression(cron); const nextDate = interval.next().toDate(); const afterNextDate = interval.next().toDate(); return Math.floor((afterNextDate.getTime() - nextDate.getTime()) / 1000); } export function validateCronAgainstScheduleLimit( cron: string, scheduleLimit: string, ): { valid: boolean; message?: string } { if (!cron) return { valid: true }; // Convert Airflow presets to cron expressions const cronExpression = convertAirflowPresetToCron(cron); // @once returns null - no recurring schedule, always valid if (cronExpression === null) { return { valid: true }; } try { const cronInterval = getCronIntervalSeconds(cronExpression); const minInterval = getMinimumIntervalSeconds(scheduleLimit); if (cronInterval < minInterval) { return { valid: false, message: `Schedule interval (${cronInterval}s) is below customer limit (${scheduleLimit}: ${minInterval}s minimum)`, }; } return { valid: true }; } catch (error) { return { valid: false, message: `Invalid cron expression: ${error.message}` }; } }