UNPKG

cron-validate

Version:

cron-validate is a cron-expression validator written in TypeScript.

253 lines (252 loc) 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); require("./index"); const result_1 = require("./result"); require("./types"); // Instead of translating the alias to a number, we just validate that it's an accepted alias. // This is to avoid managing the limits with the translation to numbers. // e.g.: For AWS, sun = 1, while for normal cron, sun = 0. Translating to numbers would break that. const monthAliases = [ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec', ]; const daysOfWeekAliases = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; const checkWildcardLimit = (cronFieldType, options) => options[cronFieldType].lowerLimit === options.preset[cronFieldType].minValue && options[cronFieldType].upperLimit === options.preset[cronFieldType].maxValue; const checkSingleElementWithinLimits = (element, cronFieldType, options) => { if (cronFieldType === 'months' && options.useAliases && monthAliases.indexOf(element.toLowerCase()) !== -1) { return (0, result_1.valid)(true); } if (cronFieldType === 'daysOfWeek' && options.useAliases && daysOfWeekAliases.indexOf(element.toLowerCase()) !== -1) { return (0, result_1.valid)(true); } const number = Number(element); if (isNaN(number)) { return (0, result_1.err)(`Element '${element} of ${cronFieldType} field is invalid.`); } // check if integer and not a decimal if (number % 1 !== 0) { return (0, result_1.err)(`Element '${element} of ${cronFieldType} field is not an integer.`); } const { lowerLimit } = options[cronFieldType]; const { upperLimit } = options[cronFieldType]; if (lowerLimit && number < lowerLimit) { return (0, result_1.err)(`Number ${number} of ${cronFieldType} field is smaller than lower limit '${lowerLimit}'`); } if (upperLimit && number > upperLimit) { return (0, result_1.err)(`Number ${number} of ${cronFieldType} field is bigger than upper limit '${upperLimit}'`); } return (0, result_1.valid)(true); }; const checkSingleElement = (element, cronFieldType, options) => { if (element === '*') { if (!checkWildcardLimit(cronFieldType, options)) { return (0, result_1.err)(`Field ${cronFieldType} uses wildcard '*', but is limited to ${options[cronFieldType].lowerLimit}-${options[cronFieldType].upperLimit}`); } return (0, result_1.valid)(true); } if (element === '') { return (0, result_1.err)(`One of the elements is empty in ${cronFieldType} field.`); } if (cronFieldType === 'daysOfMonth' && options.useLastDayOfMonth && element === 'L') { return (0, result_1.valid)(true); } // We must do that check here because L is used with a number to specify the day of the week for which // we look for the last occurrence in the month. // We use `endsWith` here because anywhere else is not valid so it will be caught later on. if (cronFieldType === 'daysOfWeek' && options.useLastDayOfWeek && element.endsWith('L')) { const day = element.slice(0, -1); if (day === '') { // This means that element is only `L` which is the equivalent of saturdayL return (0, result_1.valid)(true); } return checkSingleElementWithinLimits(day, cronFieldType, options); } // We must do that check here because W is used with a number to specify the day of the month for which // we must run over a weekday instead. // We use `endsWith` here because anywhere else is not valid so it will be caught later on. if (cronFieldType === 'daysOfMonth' && options.useNearestWeekday && element.endsWith('W')) { const day = element.slice(0, -1); if (day === '') { return (0, result_1.err)(`The 'W' must be preceded by a day`); } // Edge case where the L can be used with W to form last weekday of month if (options.useLastDayOfMonth && day === 'L') { return (0, result_1.valid)(true); } return checkSingleElementWithinLimits(day, cronFieldType, options); } if (cronFieldType === 'daysOfWeek' && options.useNthWeekdayOfMonth && element.indexOf('#') !== -1) { const [day, occurrence, ...leftOvers] = element.split('#'); if (leftOvers.length !== 0) { return (0, result_1.err)(`Unexpected number of '#' in ${element}, can only be used once.`); } const occurrenceNum = Number(occurrence); if (!occurrence || isNaN(occurrenceNum)) { return (0, result_1.err)(`Unexpected value following the '#' symbol, a positive number was expected but found ${occurrence}.`); } if (occurrenceNum > 5) { return (0, result_1.err)(`Number of occurrence of the day of the week cannot be greater than 5.`); } return checkSingleElementWithinLimits(day, cronFieldType, options); } return checkSingleElementWithinLimits(element, cronFieldType, options); }; const checkRangeElement = (element, cronFieldType, options, position) => { if (element === '*') { return (0, result_1.err)(`'*' can't be part of a range in ${cronFieldType} field.`); } if (element === '') { return (0, result_1.err)(`One of the range elements is empty in ${cronFieldType} field.`); } // We can have `L` as the first element of a range to specify an offset. if (options.useLastDayOfMonth && cronFieldType === 'daysOfMonth' && element === 'L' && position === 0) { return (0, result_1.valid)(true); } return checkSingleElementWithinLimits(element, cronFieldType, options); }; const checkFirstStepElement = (firstStepElement, cronFieldType, options) => { const rangeArray = firstStepElement.split('-'); if (rangeArray.length > 2) { return (0, result_1.err)(`List element '${firstStepElement}' is not valid. (More than one '-')`); } if (rangeArray.length === 1) { return checkSingleElement(rangeArray[0], cronFieldType, options); } if (rangeArray.length === 2) { const firstRangeElementResult = checkRangeElement(rangeArray[0], cronFieldType, options, 0); const secondRangeElementResult = checkRangeElement(rangeArray[1], cronFieldType, options, 1); if (firstRangeElementResult.isError()) { return firstRangeElementResult; } if (secondRangeElementResult.isError()) { return secondRangeElementResult; } if (Number(rangeArray[0]) > Number(rangeArray[1])) { return (0, result_1.err)(`Lower range end '${rangeArray[0]}' is bigger than upper range end '${rangeArray[1]}' of ${cronFieldType} field.`); } return (0, result_1.valid)(true); } return (0, result_1.err)('Some other error in checkFirstStepElement (rangeArray less than 1)'); }; const checkListElement = (listElement, cronFieldType, options) => { // Checks list element for steps like */2, 10-20/2 const stepArray = listElement.split('/'); if (stepArray.length > 2) { return (0, result_1.err)(`List element '${listElement}' is not valid. (More than one '/')`); } if (!options.allowStepping) { return (0, result_1.err)('Stepping (\'/\') is now allowed.'); } const firstElementResult = checkFirstStepElement(stepArray[0], cronFieldType, options); if (firstElementResult.isError()) { return firstElementResult; } if (stepArray.length === 2) { const secondStepElement = stepArray[1]; if (!secondStepElement) { return (0, result_1.err)(`Second step element '${secondStepElement}' of '${listElement}' is not valid (doesnt exist).`); } if (isNaN(Number(secondStepElement))) { return (0, result_1.err)(`Second step element '${secondStepElement}' of '${listElement}' is not valid (not a number).`); } const secondStepNumber = Number(secondStepElement); if (secondStepNumber === 0) { return (0, result_1.err)(`Second step element '${secondStepElement}' of '${listElement}' cannot be zero.`); } const { lowerLimit, upperLimit } = options[cronFieldType]; // check if step number is an integer if (secondStepNumber % 1 !== 0) { return (0, result_1.err)(`Second step element '${secondStepElement}' of '${listElement}' is not an integer.`); } // check if step number is less than the max number if (upperLimit && secondStepNumber > upperLimit) { return (0, result_1.err)(`Second step element '${secondStepElement}' of '${listElement}' is bigger than the upper limit '${upperLimit}'.`); } // check if the step is inside the allowed range, so 10-20/5 is allowed (10, 15, 20), but // 10-20/11 is not allowed, because the first value (after the initial) would be 21 but this is bigger than 20 const rangeArray = stepArray[0].split('-'); if (rangeArray.length === 2) { const rangeStart = Number(rangeArray[0]); const rangeEnd = Number(rangeArray[1]); if (!isNaN(rangeStart) && !isNaN(rangeEnd)) { if (secondStepNumber <= 0) { return (0, result_1.err)(`Step value '${secondStepElement}' must be greater than 0.`); } const customRange = rangeEnd - rangeStart + 1; if (secondStepNumber >= customRange) { return (0, result_1.err)(`Step value '${secondStepElement}' is too large for the range '${rangeStart}-${rangeEnd}'.`); } } } } return (0, result_1.valid)(true); }; const checkField = (cronField, cronFieldType, options) => { if (![ 'seconds', 'minutes', 'hours', 'daysOfMonth', 'months', 'daysOfWeek', 'years', ].includes(cronFieldType)) { return (0, result_1.err)([`Cron field type '${cronFieldType}' does not exist.`]); } // Check for blank day if (cronField === '?') { if (cronFieldType === 'daysOfMonth' || cronFieldType === 'daysOfWeek') { if (options.useBlankDay) { return (0, result_1.valid)(true); } return (0, result_1.err)([ `useBlankDay is not enabled, but is used in ${cronFieldType} field`, ]); } return (0, result_1.err)([`blank notation is not allowed in ${cronFieldType} field`]); } // Check for lists e.g. 4,5,6,8-18,20-40/2 const listArray = cronField.split(','); const checkResults = []; listArray.forEach((listElement) => { checkResults.push(checkListElement(listElement, cronFieldType, options)); }); if (checkResults.every(value => value.isValid())) { return (0, result_1.valid)(true); } const errorArray = []; checkResults.forEach(result => { if (result.isError()) { errorArray.push(result.getError()); } }); return (0, result_1.err)(errorArray); }; exports.default = checkField;