mirror of
https://github.com/dadosfera/maestro.git
synced 2026-09-27 05:24:49 +00:00
- Add cron-validation utility with Airflow presets support (@once, @daily, etc.) - Validate cron expressions against customer scheduleLimit from DUC - Fetch scheduleLimit via gRPC to ensure up-to-date configuration - Improve ES updatePipeline to fetch current doc and merge changes - Only update fields explicitly provided in PATCH request body - Preserve config.tables when updating config.cron 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
91 lines
2.7 KiB
TypeScript
91 lines
2.7 KiB
TypeScript
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<string, string | null> = {
|
|
'@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}` };
|
|
}
|
|
}
|