(expression)
| 6 | */ |
| 7 | |
| 8 | export function validateCronExpression(expression) { |
| 9 | if (!expression || expression.trim() === '') { |
| 10 | return { valid: false, error: 'Cron expression is required' }; |
| 11 | } |
| 12 | |
| 13 | const parts = expression.trim().split(/\s+/); |
| 14 | if (parts.length !== 5) { |
| 15 | return { |
| 16 | valid: false, |
| 17 | error: |
| 18 | 'Cron expression must have exactly 5 parts: minute hour day month weekday', |
| 19 | }; |
| 20 | } |
| 21 | |
| 22 | const [minute, hour, dayOfMonth, month, dayOfWeek] = parts; |
| 23 | |
| 24 | const cronPartRegex = |
| 25 | /^(\*\/\d+|\*|\d+(-\d+)?(\/\d+)?(,\d+(-\d+)?(\/\d+)?)*)$/; |
| 26 | |
| 27 | const fields = [ |
| 28 | { value: minute, label: 'minute', min: 0, max: 59 }, |
| 29 | { value: hour, label: 'hour', min: 0, max: 23 }, |
| 30 | { value: dayOfMonth, label: 'day', min: 1, max: 31 }, |
| 31 | { value: month, label: 'month', min: 1, max: 12 }, |
| 32 | { value: dayOfWeek, label: 'weekday', min: 0, max: 6 }, |
| 33 | ]; |
| 34 | |
| 35 | for (const { value, label, min, max } of fields) { |
| 36 | if (!cronPartRegex.test(value)) { |
| 37 | return { |
| 38 | valid: false, |
| 39 | error: `Invalid ${label} field (${min}-${max}, *, or cron syntax)`, |
| 40 | }; |
| 41 | } |
| 42 | |
| 43 | // Extra numeric-range check for plain numbers |
| 44 | if ( |
| 45 | !( |
| 46 | value === '*' || |
| 47 | value.includes('/') || |
| 48 | value.includes('-') || |
| 49 | value.includes(',') |
| 50 | ) |
| 51 | ) { |
| 52 | const num = parseInt(value, 10); |
| 53 | if (isNaN(num) || num < min || num > max) { |
| 54 | return { |
| 55 | valid: false, |
| 56 | error: `${label.charAt(0).toUpperCase() + label.slice(1)} must be between ${min} and ${max}`, |
| 57 | }; |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | return { valid: true, error: null }; |
| 63 | } |
no test coverage detected