feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2016 Harri Siirak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+175
View File
@@ -0,0 +1,175 @@
cron-parser
================
[![Build Status](https://github.com/harrisiirak/cron-parser/actions/workflows/push.yml/badge.svg?branch=master)](https://github.com/harrisiirak/cron-parser/actions/workflows/push.yml)
[![NPM version](https://badge.fury.io/js/cron-parser.png)](http://badge.fury.io/js/cron-parser)
Node.js library for parsing and manipulating crontab instructions. It includes support for timezones and DST transitions.
__Compatibility__
Node >= 12.0.0
TypeScript >= 4.2
Setup
========
```bash
npm install cron-parser
```
Supported format
========
```
* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ |
│ │ │ │ │ └ day of week (0 - 7, 1L - 7L) (0 or 7 is Sun)
│ │ │ │ └───── month (1 - 12)
│ │ │ └────────── day of month (1 - 31, L)
│ │ └─────────────── hour (0 - 23)
│ └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, optional)
```
Supports mixed use of ranges and range increments (W character not supported currently). See tests for examples.
Usage
========
Simple expression.
```javascript
var parser = require('cron-parser');
try {
var interval = parser.parseExpression('*/2 * * * *');
console.log('Date: ', interval.next().toString()); // Sat Dec 29 2012 00:42:00 GMT+0200 (EET)
console.log('Date: ', interval.next().toString()); // Sat Dec 29 2012 00:44:00 GMT+0200 (EET)
console.log('Date: ', interval.prev().toString()); // Sat Dec 29 2012 00:42:00 GMT+0200 (EET)
console.log('Date: ', interval.prev().toString()); // Sat Dec 29 2012 00:40:00 GMT+0200 (EET)
} catch (err) {
console.log('Error: ' + err.message);
}
```
Iteration with limited timespan. Also returns ES6 compatible iterator (when iterator flag is set to true).
```javascript
var parser = require('cron-parser');
var options = {
currentDate: new Date('Wed, 26 Dec 2012 12:38:53 UTC'),
endDate: new Date('Wed, 26 Dec 2012 14:40:00 UTC'),
iterator: true
};
try {
var interval = parser.parseExpression('*/22 * * * *', options);
while (true) {
try {
var obj = interval.next();
console.log('value:', obj.value.toString(), 'done:', obj.done);
} catch (e) {
break;
}
}
// value: Wed Dec 26 2012 14:44:00 GMT+0200 (EET) done: false
// value: Wed Dec 26 2012 15:00:00 GMT+0200 (EET) done: false
// value: Wed Dec 26 2012 15:22:00 GMT+0200 (EET) done: false
// value: Wed Dec 26 2012 15:44:00 GMT+0200 (EET) done: false
// value: Wed Dec 26 2012 16:00:00 GMT+0200 (EET) done: false
// value: Wed Dec 26 2012 16:22:00 GMT+0200 (EET) done: true
} catch (err) {
console.log('Error: ' + err.message);
}
```
Timezone support
```javascript
var parser = require('cron-parser');
var options = {
currentDate: '2016-03-27 00:00:01',
tz: 'Europe/Athens'
};
try {
var interval = parser.parseExpression('0 * * * *', options);
console.log('Date: ', interval.next().toString()); // Date: Sun Mar 27 2016 01:00:00 GMT+0200
console.log('Date: ', interval.next().toString()); // Date: Sun Mar 27 2016 02:00:00 GMT+0200
console.log('Date: ', interval.next().toString()); // Date: Sun Mar 27 2016 04:00:00 GMT+0300 (Notice DST transition)
} catch (err) {
console.log('Error: ' + err.message);
}
```
Manipulation
```javascript
var parser = require('cron-parser');
var interval = parser.parseExpression('0 7 * * 0-4');
var fields = JSON.parse(JSON.stringify(interval.fields)); // Fields is immutable
fields.hour = [8];
fields.minute = [29];
fields.dayOfWeek = [1,3,4,5,6,7];
var modifiedInterval = parser.fieldsToExpression(fields);
var cronString = modifiedInterval.stringify();
console.log(cronString); // "29 8 * * 1,3-7"
```
Options
========
* *currentDate* - Start date of the iteration
* *endDate* - End date of the iteration
`currentDate` and `endDate` accept `string`, `integer` and `Date` as input.
In case of using `string` as input, not every string format accepted
by the `Date` constructor will work correctly.
The supported formats are:
- [`ISO8601`](https://moment.github.io/luxon/#/parsing?id=iso-8601)
- [`HTTP and RFC2822`](https://moment.github.io/luxon/#/parsing?id=http-and-rfc2822)
- [`SQL`](https://moment.github.io/luxon/#/parsing?id=sql)
The reason being that those are the formats accepted by the
[`luxon`](https://moment.github.io/luxon/) library which is being used to handle dates.
Using `Date` as an input can be problematic specially when using the `tz` option. The issue being that, when creating a new `Date` object without
any timezone information, it will be created in the timezone of the system that is running the code. This (most of times) won't be what the user
will be expecting. Using one of the supported `string` formats will solve the issue(see timezone example).
* *iterator* - Return ES6 compatible iterator object
* *utc* - Enable UTC
* *tz* - Timezone string. It won't be used in case `utc` is enabled
Last weekday of the month
=========================
This library supports parsing the range `0L - 7L` in the `weekday` position of
the cron expression, where the `L` means "last occurrence of this weekday for
the month in progress".
For example, the following expression will run on the last monday of the month
at midnight:
```
0 0 0 * * 1L
```
The library also supports combining `L` expressions with other weekday
expressions. For example, the following cron will run every Monday as well
as the last Wednesday of the month:
```
0 0 0 * * 1,3L
```
+252
View File
@@ -0,0 +1,252 @@
'use strict';
var luxon = require('luxon');
CronDate.prototype.addYear = function() {
this._date = this._date.plus({ years: 1 });
};
CronDate.prototype.addMonth = function() {
this._date = this._date.plus({ months: 1 }).startOf('month');
};
CronDate.prototype.addDay = function() {
this._date = this._date.plus({ days: 1 }).startOf('day');
};
CronDate.prototype.addHour = function() {
var prev = this._date;
this._date = this._date.plus({ hours: 1 }).startOf('hour');
if (this._date <= prev) {
this._date = this._date.plus({ hours: 1 });
}
};
CronDate.prototype.addMinute = function() {
var prev = this._date;
this._date = this._date.plus({ minutes: 1 }).startOf('minute');
if (this._date < prev) {
this._date = this._date.plus({ hours: 1 });
}
};
CronDate.prototype.addSecond = function() {
var prev = this._date;
this._date = this._date.plus({ seconds: 1 }).startOf('second');
if (this._date < prev) {
this._date = this._date.plus({ hours: 1 });
}
};
CronDate.prototype.subtractYear = function() {
this._date = this._date.minus({ years: 1 });
};
CronDate.prototype.subtractMonth = function() {
this._date = this._date
.minus({ months: 1 })
.endOf('month')
.startOf('second');
};
CronDate.prototype.subtractDay = function() {
this._date = this._date
.minus({ days: 1 })
.endOf('day')
.startOf('second');
};
CronDate.prototype.subtractHour = function() {
var prev = this._date;
this._date = this._date
.minus({ hours: 1 })
.endOf('hour')
.startOf('second');
if (this._date >= prev) {
this._date = this._date.minus({ hours: 1 });
}
};
CronDate.prototype.subtractMinute = function() {
var prev = this._date;
this._date = this._date.minus({ minutes: 1 })
.endOf('minute')
.startOf('second');
if (this._date > prev) {
this._date = this._date.minus({ hours: 1 });
}
};
CronDate.prototype.subtractSecond = function() {
var prev = this._date;
this._date = this._date
.minus({ seconds: 1 })
.startOf('second');
if (this._date > prev) {
this._date = this._date.minus({ hours: 1 });
}
};
CronDate.prototype.getDate = function() {
return this._date.day;
};
CronDate.prototype.getFullYear = function() {
return this._date.year;
};
CronDate.prototype.getDay = function() {
var weekday = this._date.weekday;
return weekday == 7 ? 0 : weekday;
};
CronDate.prototype.getMonth = function() {
return this._date.month - 1;
};
CronDate.prototype.getHours = function() {
return this._date.hour;
};
CronDate.prototype.getMinutes = function() {
return this._date.minute;
};
CronDate.prototype.getSeconds = function() {
return this._date.second;
};
CronDate.prototype.getMilliseconds = function() {
return this._date.millisecond;
};
CronDate.prototype.getTime = function() {
return this._date.valueOf();
};
CronDate.prototype.getUTCDate = function() {
return this._getUTC().day;
};
CronDate.prototype.getUTCFullYear = function() {
return this._getUTC().year;
};
CronDate.prototype.getUTCDay = function() {
var weekday = this._getUTC().weekday;
return weekday == 7 ? 0 : weekday;
};
CronDate.prototype.getUTCMonth = function() {
return this._getUTC().month - 1;
};
CronDate.prototype.getUTCHours = function() {
return this._getUTC().hour;
};
CronDate.prototype.getUTCMinutes = function() {
return this._getUTC().minute;
};
CronDate.prototype.getUTCSeconds = function() {
return this._getUTC().second;
};
CronDate.prototype.toISOString = function() {
return this._date.toUTC().toISO();
};
CronDate.prototype.toJSON = function() {
return this._date.toJSON();
};
CronDate.prototype.setDate = function(d) {
this._date = this._date.set({ day: d });
};
CronDate.prototype.setFullYear = function(y) {
this._date = this._date.set({ year: y });
};
CronDate.prototype.setDay = function(d) {
this._date = this._date.set({ weekday: d });
};
CronDate.prototype.setMonth = function(m) {
this._date = this._date.set({ month: m + 1 });
};
CronDate.prototype.setHours = function(h) {
this._date = this._date.set({ hour: h });
};
CronDate.prototype.setMinutes = function(m) {
this._date = this._date.set({ minute: m });
};
CronDate.prototype.setSeconds = function(s) {
this._date = this._date.set({ second: s });
};
CronDate.prototype.setMilliseconds = function(s) {
this._date = this._date.set({ millisecond: s });
};
CronDate.prototype._getUTC = function() {
return this._date.toUTC();
};
CronDate.prototype.toString = function() {
return this.toDate().toString();
};
CronDate.prototype.toDate = function() {
return this._date.toJSDate();
};
CronDate.prototype.isLastDayOfMonth = function() {
//next day
var newDate = this._date.plus({ days: 1 }).startOf('day');
return this._date.month !== newDate.month;
};
/**
* Returns true when the current weekday is the last occurrence of this weekday
* for the present month.
*/
CronDate.prototype.isLastWeekdayOfMonth = function() {
// Check this by adding 7 days to the current date and seeing if it's
// a different month
var newDate = this._date.plus({ days: 7 }).startOf('day');
return this._date.month !== newDate.month;
};
function CronDate (timestamp, tz) {
var dateOpts = { zone: tz };
if (!timestamp) {
this._date = luxon.DateTime.local();
} else if (timestamp instanceof CronDate) {
this._date = timestamp._date;
} else if (timestamp instanceof Date) {
this._date = luxon.DateTime.fromJSDate(timestamp, dateOpts);
} else if (typeof timestamp === 'number') {
this._date = luxon.DateTime.fromMillis(timestamp, dateOpts);
} else if (typeof timestamp === 'string') {
this._date = luxon.DateTime.fromISO(timestamp, dateOpts);
this._date.isValid || (this._date = luxon.DateTime.fromRFC2822(timestamp, dateOpts));
this._date.isValid || (this._date = luxon.DateTime.fromSQL(timestamp, dateOpts));
// RFC2822-like format without the required timezone offset (used in tests)
this._date.isValid || (this._date = luxon.DateTime.fromFormat(timestamp, 'EEE, d MMM yyyy HH:mm:ss', dateOpts));
}
if (!this._date || !this._date.isValid) {
throw new Error('CronDate: unhandled timestamp: ' + JSON.stringify(timestamp));
}
if (tz && tz !== this._date.zoneName) {
this._date = this._date.setZone(tz);
}
}
module.exports = CronDate;
+1002
View File
@@ -0,0 +1,1002 @@
'use strict';
// Load Date class extensions
var CronDate = require('./date');
var stringifyField = require('./field_stringify');
/**
* Cron iteration loop safety limit
*/
var LOOP_LIMIT = 10000;
/**
* Construct a new expression parser
*
* Options:
* currentDate: iterator start date
* endDate: iterator end date
*
* @constructor
* @private
* @param {Object} fields Expression fields parsed values
* @param {Object} options Parser options
*/
function CronExpression (fields, options) {
this._options = options;
this._utc = options.utc || false;
this._tz = this._utc ? 'UTC' : options.tz;
this._currentDate = new CronDate(options.currentDate, this._tz);
this._startDate = options.startDate ? new CronDate(options.startDate, this._tz) : null;
this._endDate = options.endDate ? new CronDate(options.endDate, this._tz) : null;
this._isIterator = options.iterator || false;
this._hasIterated = false;
this._nthDayOfWeek = options.nthDayOfWeek || 0;
this.fields = CronExpression._freezeFields(fields);
}
/**
* Field mappings
* @type {Array}
*/
CronExpression.map = [ 'second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek' ];
/**
* Prefined intervals
* @type {Object}
*/
CronExpression.predefined = {
'@yearly': '0 0 1 1 *',
'@monthly': '0 0 1 * *',
'@weekly': '0 0 * * 0',
'@daily': '0 0 * * *',
'@hourly': '0 * * * *'
};
/**
* Fields constraints
* @type {Array}
*/
CronExpression.constraints = [
{ min: 0, max: 59, chars: [] }, // Second
{ min: 0, max: 59, chars: [] }, // Minute
{ min: 0, max: 23, chars: [] }, // Hour
{ min: 1, max: 31, chars: ['L'] }, // Day of month
{ min: 1, max: 12, chars: [] }, // Month
{ min: 0, max: 7, chars: ['L'] }, // Day of week
];
/**
* Days in month
* @type {number[]}
*/
CronExpression.daysInMonth = [
31,
29,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31
];
/**
* Field aliases
* @type {Object}
*/
CronExpression.aliases = {
month: {
jan: 1,
feb: 2,
mar: 3,
apr: 4,
may: 5,
jun: 6,
jul: 7,
aug: 8,
sep: 9,
oct: 10,
nov: 11,
dec: 12
},
dayOfWeek: {
sun: 0,
mon: 1,
tue: 2,
wed: 3,
thu: 4,
fri: 5,
sat: 6
}
};
/**
* Field defaults
* @type {Array}
*/
CronExpression.parseDefaults = [ '0', '*', '*', '*', '*', '*' ];
CronExpression.standardValidCharacters = /^[,*\d/-]+$/;
CronExpression.dayOfWeekValidCharacters = /^[?,*\dL#/-]+$/;
CronExpression.dayOfMonthValidCharacters = /^[?,*\dL/-]+$/;
CronExpression.validCharacters = {
second: CronExpression.standardValidCharacters,
minute: CronExpression.standardValidCharacters,
hour: CronExpression.standardValidCharacters,
dayOfMonth: CronExpression.dayOfMonthValidCharacters,
month: CronExpression.standardValidCharacters,
dayOfWeek: CronExpression.dayOfWeekValidCharacters,
};
CronExpression._isValidConstraintChar = function _isValidConstraintChar(constraints, value) {
if (typeof value !== 'string') {
return false;
}
return constraints.chars.some(function(char) {
return value.indexOf(char) > -1;
});
};
/**
* Parse input interval
*
* @param {String} field Field symbolic name
* @param {String} value Field value
* @param {Array} constraints Range upper and lower constraints
* @return {Array} Sequence of sorted values
* @private
*/
CronExpression._parseField = function _parseField (field, value, constraints) {
// Replace aliases
switch (field) {
case 'month':
case 'dayOfWeek':
var aliases = CronExpression.aliases[field];
value = value.replace(/[a-z]{3}/gi, function(match) {
match = match.toLowerCase();
if (typeof aliases[match] !== 'undefined') {
return aliases[match];
} else {
throw new Error('Validation error, cannot resolve alias "' + match + '"');
}
});
break;
}
// Check for valid characters.
if (!(CronExpression.validCharacters[field].test(value))) {
throw new Error('Invalid characters, got value: ' + value);
}
// Replace '*' and '?'
if (value.indexOf('*') !== -1) {
value = value.replace(/\*/g, constraints.min + '-' + constraints.max);
} else if (value.indexOf('?') !== -1) {
value = value.replace(/\?/g, constraints.min + '-' + constraints.max);
}
//
// Inline parsing functions
//
// Parser path:
// - parseSequence
// - parseRepeat
// - parseRange
/**
* Parse sequence
*
* @param {String} val
* @return {Array}
* @private
*/
function parseSequence (val) {
var stack = [];
function handleResult (result) {
if (result instanceof Array) { // Make sequence linear
for (var i = 0, c = result.length; i < c; i++) {
var value = result[i];
if (CronExpression._isValidConstraintChar(constraints, value)) {
stack.push(value);
continue;
}
// Check constraints
if (typeof value !== 'number' || Number.isNaN(value) || value < constraints.min || value > constraints.max) {
throw new Error(
'Constraint error, got value ' + value + ' expected range ' +
constraints.min + '-' + constraints.max
);
}
stack.push(value);
}
} else { // Scalar value
if (CronExpression._isValidConstraintChar(constraints, result)) {
stack.push(result);
return;
}
var numResult = +result;
// Check constraints
if (Number.isNaN(numResult) || numResult < constraints.min || numResult > constraints.max) {
throw new Error(
'Constraint error, got value ' + result + ' expected range ' +
constraints.min + '-' + constraints.max
);
}
if (field === 'dayOfWeek') {
numResult = numResult % 7;
}
stack.push(numResult);
}
}
var atoms = val.split(',');
if (!atoms.every(function (atom) {
return atom.length > 0;
})) {
throw new Error('Invalid list value format');
}
if (atoms.length > 1) {
for (var i = 0, c = atoms.length; i < c; i++) {
handleResult(parseRepeat(atoms[i]));
}
} else {
handleResult(parseRepeat(val));
}
stack.sort(CronExpression._sortCompareFn);
return stack;
}
/**
* Parse repetition interval
*
* @param {String} val
* @return {Array}
*/
function parseRepeat (val) {
var repeatInterval = 1;
var atoms = val.split('/');
if (atoms.length > 2) {
throw new Error('Invalid repeat: ' + val);
}
if (atoms.length > 1) {
if (atoms[0] == +atoms[0]) {
atoms = [atoms[0] + '-' + constraints.max, atoms[1]];
}
return parseRange(atoms[0], atoms[atoms.length - 1]);
}
return parseRange(val, repeatInterval);
}
/**
* Parse range
*
* @param {String} val
* @param {Number} repeatInterval Repetition interval
* @return {Array}
* @private
*/
function parseRange (val, repeatInterval) {
var stack = [];
var atoms = val.split('-');
if (atoms.length > 1 ) {
// Invalid range, return value
if (atoms.length < 2) {
return +val;
}
if (!atoms[0].length) {
if (!atoms[1].length) {
throw new Error('Invalid range: ' + val);
}
return +val;
}
// Validate range
var min = +atoms[0];
var max = +atoms[1];
if (Number.isNaN(min) || Number.isNaN(max) ||
min < constraints.min || max > constraints.max) {
throw new Error(
'Constraint error, got range ' +
min + '-' + max +
' expected range ' +
constraints.min + '-' + constraints.max
);
} else if (min > max) {
throw new Error('Invalid range: ' + val);
}
// Create range
var repeatIndex = +repeatInterval;
if (Number.isNaN(repeatIndex) || repeatIndex <= 0) {
throw new Error('Constraint error, cannot repeat at every ' + repeatIndex + ' time.');
}
// JS DOW is in range of 0-6 (SUN-SAT) but we also support 7 in the expression
// Handle case when range contains 7 instead of 0 and translate this value to 0
if (field === 'dayOfWeek' && max % 7 === 0) {
stack.push(0);
}
for (var index = min, count = max; index <= count; index++) {
var exists = stack.indexOf(index) !== -1;
if (!exists && repeatIndex > 0 && (repeatIndex % repeatInterval) === 0) {
repeatIndex = 1;
stack.push(index);
} else {
repeatIndex++;
}
}
return stack;
}
return Number.isNaN(+val) ? val : +val;
}
return parseSequence(value);
};
CronExpression._sortCompareFn = function(a, b) {
var aIsNumber = typeof a === 'number';
var bIsNumber = typeof b === 'number';
if (aIsNumber && bIsNumber) {
return a - b;
}
if (!aIsNumber && bIsNumber) {
return 1;
}
if (aIsNumber && !bIsNumber) {
return -1;
}
return a.localeCompare(b);
};
CronExpression._handleMaxDaysInMonth = function(mappedFields) {
// Filter out any day of month value that is larger than given month expects
if (mappedFields.month.length === 1) {
var daysInMonth = CronExpression.daysInMonth[mappedFields.month[0] - 1];
if (mappedFields.dayOfMonth[0] > daysInMonth) {
throw new Error('Invalid explicit day of month definition');
}
return mappedFields.dayOfMonth
.filter(function(dayOfMonth) {
return dayOfMonth === 'L' ? true : dayOfMonth <= daysInMonth;
})
.sort(CronExpression._sortCompareFn);
}
};
CronExpression._freezeFields = function(fields) {
for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
var field = CronExpression.map[i]; // Field name
var value = fields[field];
fields[field] = Object.freeze(value);
}
return Object.freeze(fields);
};
CronExpression.prototype._applyTimezoneShift = function(currentDate, dateMathVerb, method) {
if ((method === 'Month') || (method === 'Day')) {
var prevTime = currentDate.getTime();
currentDate[dateMathVerb + method]();
var currTime = currentDate.getTime();
if (prevTime === currTime) {
// Jumped into a not existent date due to a DST transition
if ((currentDate.getMinutes() === 0) &&
(currentDate.getSeconds() === 0)) {
currentDate.addHour();
} else if ((currentDate.getMinutes() === 59) &&
(currentDate.getSeconds() === 59)) {
currentDate.subtractHour();
}
}
} else {
var previousHour = currentDate.getHours();
currentDate[dateMathVerb + method]();
var currentHour = currentDate.getHours();
var diff = currentHour - previousHour;
if (diff === 2) {
// Starting DST
if (this.fields.hour.length !== 24) {
// Hour is specified
this._dstStart = currentHour;
}
} else if ((diff === 0) &&
(currentDate.getMinutes() === 0) &&
(currentDate.getSeconds() === 0)) {
// Ending DST
if (this.fields.hour.length !== 24) {
// Hour is specified
this._dstEnd = currentHour;
}
}
}
};
/**
* Find next or previous matching schedule date
*
* @return {CronDate}
* @private
*/
CronExpression.prototype._findSchedule = function _findSchedule (reverse) {
/**
* Match field value
*
* @param {String} value
* @param {Array} sequence
* @return {Boolean}
* @private
*/
function matchSchedule (value, sequence) {
for (var i = 0, c = sequence.length; i < c; i++) {
if (sequence[i] >= value) {
return sequence[i] === value;
}
}
return sequence[0] === value;
}
/**
* Helps determine if the provided date is the correct nth occurence of the
* desired day of week.
*
* @param {CronDate} date
* @param {Number} nthDayOfWeek
* @return {Boolean}
* @private
*/
function isNthDayMatch(date, nthDayOfWeek) {
if (nthDayOfWeek < 6) {
if (
date.getDate() < 8 &&
nthDayOfWeek === 1 // First occurence has to happen in first 7 days of the month
) {
return true;
}
var offset = date.getDate() % 7 ? 1 : 0; // Math is off by 1 when dayOfWeek isn't divisible by 7
var adjustedDate = date.getDate() - (date.getDate() % 7); // find the first occurance
var occurrence = Math.floor(adjustedDate / 7) + offset;
return occurrence === nthDayOfWeek;
}
return false;
}
/**
* Helper function that checks if 'L' is in the array
*
* @param {Array} expressions
*/
function isLInExpressions(expressions) {
return expressions.length > 0 && expressions.some(function(expression) {
return typeof expression === 'string' && expression.indexOf('L') >= 0;
});
}
// Whether to use backwards directionality when searching
reverse = reverse || false;
var dateMathVerb = reverse ? 'subtract' : 'add';
var currentDate = new CronDate(this._currentDate, this._tz);
var startDate = this._startDate;
var endDate = this._endDate;
// Find matching schedule
var startTimestamp = currentDate.getTime();
var stepCount = 0;
function isLastWeekdayOfMonthMatch(expressions) {
return expressions.some(function(expression) {
// There might be multiple expressions and not all of them will contain
// the "L".
if (!isLInExpressions([expression])) {
return false;
}
// The first character represents the weekday
var weekday = Number.parseInt(expression[0]) % 7;
if (Number.isNaN(weekday)) {
throw new Error('Invalid last weekday of the month expression: ' + expression);
}
return currentDate.getDay() === weekday && currentDate.isLastWeekdayOfMonth();
});
}
while (stepCount < LOOP_LIMIT) {
stepCount++;
// Validate timespan
if (reverse) {
if (startDate && (currentDate.getTime() - startDate.getTime() < 0)) {
throw new Error('Out of the timespan range');
}
} else {
if (endDate && (endDate.getTime() - currentDate.getTime()) < 0) {
throw new Error('Out of the timespan range');
}
}
// Day of month and week matching:
//
// "The day of a command's execution can be specified by two fields --
// day of month, and day of week. If both fields are restricted (ie,
// aren't *), the command will be run when either field matches the cur-
// rent time. For example, "30 4 1,15 * 5" would cause a command to be
// run at 4:30 am on the 1st and 15th of each month, plus every Friday."
//
// http://unixhelp.ed.ac.uk/CGI/man-cgi?crontab+5
//
var dayOfMonthMatch = matchSchedule(currentDate.getDate(), this.fields.dayOfMonth);
if (isLInExpressions(this.fields.dayOfMonth)) {
dayOfMonthMatch = dayOfMonthMatch || currentDate.isLastDayOfMonth();
}
var dayOfWeekMatch = matchSchedule(currentDate.getDay(), this.fields.dayOfWeek);
if (isLInExpressions(this.fields.dayOfWeek)) {
dayOfWeekMatch = dayOfWeekMatch || isLastWeekdayOfMonthMatch(this.fields.dayOfWeek);
}
var isDayOfMonthWildcardMatch = this.fields.dayOfMonth.length >= CronExpression.daysInMonth[currentDate.getMonth()];
var isDayOfWeekWildcardMatch = this.fields.dayOfWeek.length === CronExpression.constraints[5].max - CronExpression.constraints[5].min + 1;
var currentHour = currentDate.getHours();
// Add or subtract day if select day not match with month (according to calendar)
if (!dayOfMonthMatch && (!dayOfWeekMatch || isDayOfWeekWildcardMatch)) {
this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
continue;
}
// Add or subtract day if not day of month is set (and no match) and day of week is wildcard
if (!isDayOfMonthWildcardMatch && isDayOfWeekWildcardMatch && !dayOfMonthMatch) {
this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
continue;
}
// Add or subtract day if not day of week is set (and no match) and day of month is wildcard
if (isDayOfMonthWildcardMatch && !isDayOfWeekWildcardMatch && !dayOfWeekMatch) {
this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
continue;
}
// Add or subtract day if day of week & nthDayOfWeek are set (and no match)
if (
this._nthDayOfWeek > 0 &&
!isNthDayMatch(currentDate, this._nthDayOfWeek)
) {
this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
continue;
}
// Match month
if (!matchSchedule(currentDate.getMonth() + 1, this.fields.month)) {
this._applyTimezoneShift(currentDate, dateMathVerb, 'Month');
continue;
}
// Match hour
if (!matchSchedule(currentHour, this.fields.hour)) {
if (this._dstStart !== currentHour) {
this._dstStart = null;
this._applyTimezoneShift(currentDate, dateMathVerb, 'Hour');
continue;
} else if (!matchSchedule(currentHour - 1, this.fields.hour)) {
currentDate[dateMathVerb + 'Hour']();
continue;
}
} else if (this._dstEnd === currentHour) {
if (!reverse) {
this._dstEnd = null;
this._applyTimezoneShift(currentDate, 'add', 'Hour');
continue;
}
}
// Match minute
if (!matchSchedule(currentDate.getMinutes(), this.fields.minute)) {
this._applyTimezoneShift(currentDate, dateMathVerb, 'Minute');
continue;
}
// Match second
if (!matchSchedule(currentDate.getSeconds(), this.fields.second)) {
this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
continue;
}
// Increase a second in case in the first iteration the currentDate was not
// modified
if (startTimestamp === currentDate.getTime()) {
if ((dateMathVerb === 'add') || (currentDate.getMilliseconds() === 0)) {
this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
} else {
currentDate.setMilliseconds(0);
}
continue;
}
break;
}
if (stepCount >= LOOP_LIMIT) {
throw new Error('Invalid expression, loop limit exceeded');
}
this._currentDate = new CronDate(currentDate, this._tz);
this._hasIterated = true;
return currentDate;
};
/**
* Find next suitable date
*
* @public
* @return {CronDate|Object}
*/
CronExpression.prototype.next = function next () {
var schedule = this._findSchedule();
// Try to return ES6 compatible iterator
if (this._isIterator) {
return {
value: schedule,
done: !this.hasNext()
};
}
return schedule;
};
/**
* Find previous suitable date
*
* @public
* @return {CronDate|Object}
*/
CronExpression.prototype.prev = function prev () {
var schedule = this._findSchedule(true);
// Try to return ES6 compatible iterator
if (this._isIterator) {
return {
value: schedule,
done: !this.hasPrev()
};
}
return schedule;
};
/**
* Check if next suitable date exists
*
* @public
* @return {Boolean}
*/
CronExpression.prototype.hasNext = function() {
var current = this._currentDate;
var hasIterated = this._hasIterated;
try {
this._findSchedule();
return true;
} catch (err) {
return false;
} finally {
this._currentDate = current;
this._hasIterated = hasIterated;
}
};
/**
* Check if previous suitable date exists
*
* @public
* @return {Boolean}
*/
CronExpression.prototype.hasPrev = function() {
var current = this._currentDate;
var hasIterated = this._hasIterated;
try {
this._findSchedule(true);
return true;
} catch (err) {
return false;
} finally {
this._currentDate = current;
this._hasIterated = hasIterated;
}
};
/**
* Iterate over expression iterator
*
* @public
* @param {Number} steps Numbers of steps to iterate
* @param {Function} callback Optional callback
* @return {Array} Array of the iterated results
*/
CronExpression.prototype.iterate = function iterate (steps, callback) {
var dates = [];
if (steps >= 0) {
for (var i = 0, c = steps; i < c; i++) {
try {
var item = this.next();
dates.push(item);
// Fire the callback
if (callback) {
callback(item, i);
}
} catch (err) {
break;
}
}
} else {
for (var i = 0, c = steps; i > c; i--) {
try {
var item = this.prev();
dates.push(item);
// Fire the callback
if (callback) {
callback(item, i);
}
} catch (err) {
break;
}
}
}
return dates;
};
/**
* Reset expression iterator state
*
* @public
*/
CronExpression.prototype.reset = function reset (newDate) {
this._currentDate = new CronDate(newDate || this._options.currentDate);
};
/**
* Stringify the expression
*
* @public
* @param {Boolean} [includeSeconds] Should stringify seconds
* @return {String}
*/
CronExpression.prototype.stringify = function stringify(includeSeconds) {
var resultArr = [];
for (var i = includeSeconds ? 0 : 1, c = CronExpression.map.length; i < c; ++i) {
var field = CronExpression.map[i];
var value = this.fields[field];
var constraint = CronExpression.constraints[i];
if (field === 'dayOfMonth' && this.fields.month.length === 1) {
constraint = { min: 1, max: CronExpression.daysInMonth[this.fields.month[0] - 1] };
} else if (field === 'dayOfWeek') {
// Prefer 0-6 range when serializing day of week field
constraint = { min: 0, max: 6 };
value = value[value.length - 1] === 7 ? value.slice(0, -1) : value;
}
resultArr.push(stringifyField(value, constraint.min, constraint.max));
}
return resultArr.join(' ');
};
/**
* Parse input expression (async)
*
* @public
* @param {String} expression Input expression
* @param {Object} [options] Parsing options
*/
CronExpression.parse = function parse(expression, options) {
var self = this;
if (typeof options === 'function') {
options = {};
}
function parse (expression, options) {
if (!options) {
options = {};
}
if (typeof options.currentDate === 'undefined') {
options.currentDate = new CronDate(undefined, self._tz);
}
// Is input expression predefined?
if (CronExpression.predefined[expression]) {
expression = CronExpression.predefined[expression];
}
// Split fields
var fields = [];
var atoms = (expression + '').trim().split(/\s+/);
if (atoms.length > 6) {
throw new Error('Invalid cron expression');
}
// Resolve fields
var start = (CronExpression.map.length - atoms.length);
for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
var field = CronExpression.map[i]; // Field name
var value = atoms[atoms.length > c ? i : i - start]; // Field value
if (i < start || !value) { // Use default value
fields.push(CronExpression._parseField(
field,
CronExpression.parseDefaults[i],
CronExpression.constraints[i]
)
);
} else {
var val = field === 'dayOfWeek' ? parseNthDay(value) : value;
fields.push(CronExpression._parseField(
field,
val,
CronExpression.constraints[i]
)
);
}
}
var mappedFields = {};
for (var i = 0, c = CronExpression.map.length; i < c; i++) {
var key = CronExpression.map[i];
mappedFields[key] = fields[i];
}
var dayOfMonth = CronExpression._handleMaxDaysInMonth(mappedFields);
mappedFields.dayOfMonth = dayOfMonth || mappedFields.dayOfMonth;
return new CronExpression(mappedFields, options);
/**
* Parses out the # special character for the dayOfWeek field & adds it to options.
*
* @param {String} val
* @return {String}
* @private
*/
function parseNthDay(val) {
var atoms = val.split('#');
if (atoms.length > 1) {
var nthValue = +atoms[atoms.length - 1];
if(/,/.test(val)) {
throw new Error('Constraint error, invalid dayOfWeek `#` and `,` '
+ 'special characters are incompatible');
}
if(/\//.test(val)) {
throw new Error('Constraint error, invalid dayOfWeek `#` and `/` '
+ 'special characters are incompatible');
}
if(/-/.test(val)) {
throw new Error('Constraint error, invalid dayOfWeek `#` and `-` '
+ 'special characters are incompatible');
}
if (atoms.length > 2 || Number.isNaN(nthValue) || (nthValue < 1 || nthValue > 5)) {
throw new Error('Constraint error, invalid dayOfWeek occurrence number (#)');
}
options.nthDayOfWeek = nthValue;
return atoms[0];
}
return val;
}
}
return parse(expression, options);
};
/**
* Convert cron fields back to Cron Expression
*
* @public
* @param {Object} fields Input fields
* @param {Object} [options] Parsing options
* @return {Object}
*/
CronExpression.fieldsToExpression = function fieldsToExpression(fields, options) {
function validateConstraints (field, values, constraints) {
if (!values) {
throw new Error('Validation error, Field ' + field + ' is missing');
}
if (values.length === 0) {
throw new Error('Validation error, Field ' + field + ' contains no values');
}
for (var i = 0, c = values.length; i < c; i++) {
var value = values[i];
if (CronExpression._isValidConstraintChar(constraints, value)) {
continue;
}
// Check constraints
if (typeof value !== 'number' || Number.isNaN(value) || value < constraints.min || value > constraints.max) {
throw new Error(
'Constraint error, got value ' + value + ' expected range ' +
constraints.min + '-' + constraints.max
);
}
}
}
var mappedFields = {};
for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
var field = CronExpression.map[i]; // Field name
var values = fields[field];
validateConstraints(
field,
values,
CronExpression.constraints[i]
);
var copy = [];
var j = -1;
while (++j < values.length) {
copy[j] = values[j];
}
values = copy.sort(CronExpression._sortCompareFn)
.filter(function(item, pos, ary) {
return !pos || item !== ary[pos - 1];
});
if (values.length !== copy.length) {
throw new Error('Validation error, Field ' + field + ' contains duplicate values');
}
mappedFields[field] = values;
}
var dayOfMonth = CronExpression._handleMaxDaysInMonth(mappedFields);
mappedFields.dayOfMonth = dayOfMonth || mappedFields.dayOfMonth;
return new CronExpression(mappedFields, options || {});
};
module.exports = CronExpression;
+70
View File
@@ -0,0 +1,70 @@
'use strict';
function buildRange(item) {
return {
start: item,
count: 1
};
}
function completeRangeWithItem(range, item) {
range.end = item;
range.step = item - range.start;
range.count = 2;
}
function finalizeCurrentRange(results, currentRange, currentItemRange) {
if (currentRange) {
// Two elements do not form a range so split them into 2 single elements
if (currentRange.count === 2) {
results.push(buildRange(currentRange.start));
results.push(buildRange(currentRange.end));
} else {
results.push(currentRange);
}
}
if (currentItemRange) {
results.push(currentItemRange);
}
}
function compactField(arr) {
var results = [];
var currentRange = undefined;
for (var i = 0; i < arr.length; i++) {
var currentItem = arr[i];
if (typeof currentItem !== 'number') {
// String elements can't form a range
finalizeCurrentRange(results, currentRange, buildRange(currentItem));
currentRange = undefined;
} else if (!currentRange) {
// Start a new range
currentRange = buildRange(currentItem);
} else if (currentRange.count === 1) {
// Guess that the current item starts a range
completeRangeWithItem(currentRange, currentItem);
} else {
if (currentRange.step === currentItem - currentRange.end) {
// We found another item that matches the current range
currentRange.count++;
currentRange.end = currentItem;
} else if (currentRange.count === 2) { // The current range can't be continued
// Break the first item of the current range into a single element, and try to start a new range with the second item
results.push(buildRange(currentRange.start));
currentRange = buildRange(currentRange.end);
completeRangeWithItem(currentRange, currentItem);
} else {
// Persist the current range and start a new one with current item
finalizeCurrentRange(results, currentRange);
currentRange = buildRange(currentItem);
}
}
}
finalizeCurrentRange(results, currentRange);
return results;
}
module.exports = compactField;
+58
View File
@@ -0,0 +1,58 @@
'use strict';
var compactField = require('./field_compactor');
function stringifyField(arr, min, max) {
var ranges = compactField(arr);
if (ranges.length === 1) {
var singleRange = ranges[0];
var step = singleRange.step;
if (step === 1 && singleRange.start === min && singleRange.end === max) {
return '*';
}
if (step !== 1 && singleRange.start === min && singleRange.end === max - step + 1) {
return '*/' + step;
}
}
var result = [];
for (var i = 0, l = ranges.length; i < l; ++i) {
var range = ranges[i];
if (range.count === 1) {
result.push(range.start);
continue;
}
var step = range.step;
if (range.step === 1) {
result.push(range.start + '-' + range.end);
continue;
}
var multiplier = range.start == 0 ? range.count - 1 : range.count;
if (range.step * multiplier > range.end) {
result = result.concat(
Array
.from({ length: range.end - range.start + 1 })
.map(function (_, index) {
var value = range.start + index;
if ((value - range.start) % range.step === 0) {
return value;
}
return null;
})
.filter(function (value) {
return value != null;
})
);
} else if (range.end === max - range.step + 1) {
result.push(range.start + '/' + range.step);
} else {
result.push(range.start + '-' + range.end + '/' + range.step);
}
}
return result.join(',');
}
module.exports = stringifyField;
+116
View File
@@ -0,0 +1,116 @@
'use strict';
var CronExpression = require('./expression');
function CronParser() {}
/**
* Parse crontab entry
*
* @private
* @param {String} entry Crontab file entry/line
*/
CronParser._parseEntry = function _parseEntry (entry) {
var atoms = entry.split(' ');
if (atoms.length === 6) {
return {
interval: CronExpression.parse(entry)
};
} else if (atoms.length > 6) {
return {
interval: CronExpression.parse(
atoms.slice(0, 6).join(' ')
),
command: atoms.slice(6, atoms.length)
};
} else {
throw new Error('Invalid entry: ' + entry);
}
};
/**
* Wrapper for CronExpression.parser method
*
* @public
* @param {String} expression Input expression
* @param {Object} [options] Parsing options
* @return {Object}
*/
CronParser.parseExpression = function parseExpression (expression, options) {
return CronExpression.parse(expression, options);
};
/**
* Wrapper for CronExpression.fieldsToExpression method
*
* @public
* @param {Object} fields Input fields
* @param {Object} [options] Parsing options
* @return {Object}
*/
CronParser.fieldsToExpression = function fieldsToExpression (fields, options) {
return CronExpression.fieldsToExpression(fields, options);
};
/**
* Parse content string
*
* @public
* @param {String} data Crontab content
* @return {Object}
*/
CronParser.parseString = function parseString (data) {
var blocks = data.split('\n');
var response = {
variables: {},
expressions: [],
errors: {}
};
for (var i = 0, c = blocks.length; i < c; i++) {
var block = blocks[i];
var matches = null;
var entry = block.trim(); // Remove surrounding spaces
if (entry.length > 0) {
if (entry.match(/^#/)) { // Comment
continue;
} else if ((matches = entry.match(/^(.*)=(.*)$/))) { // Variable
response.variables[matches[1]] = matches[2];
} else { // Expression?
var result = null;
try {
result = CronParser._parseEntry('0 ' + entry);
response.expressions.push(result.interval);
} catch (err) {
response.errors[entry] = err;
}
}
}
}
return response;
};
/**
* Parse crontab file
*
* @public
* @param {String} filePath Path to file
* @param {Function} callback
*/
CronParser.parseFile = function parseFile (filePath, callback) {
require('fs').readFile(filePath, function(err, data) {
if (err) {
callback(err);
return;
}
return callback(null, CronParser.parseString(data.toString()));
});
};
module.exports = CronParser;
+91
View File
@@ -0,0 +1,91 @@
{
"name": "cron-parser",
"version": "4.9.0",
"description": "Node.js library for parsing crontab instructions",
"main": "lib/parser.js",
"types": "types/index.d.ts",
"typesVersions": {
"<4.1": {
"*": [
"types/ts3/*"
]
}
},
"directories": {
"test": "test"
},
"scripts": {
"test:tsd": "tsd",
"test:unit": "TZ=UTC tap ./test/*.js",
"test:cover": "TZ=UTC tap --coverage-report=html ./test/*.js",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "npm run lint && npm run test:unit && npm run test:tsd"
},
"repository": {
"type": "git",
"url": "https://github.com/harrisiirak/cron-parser.git"
},
"keywords": [
"cron",
"crontab",
"parser"
],
"author": "Harri Siirak",
"contributors": [
"Nicholas Clawson",
"Daniel Prentis <daniel@salsitasoft.com>",
"Renault John Lecoultre",
"Richard Astbury <richard.astbury@gmail.com>",
"Meaglin Wasabi <Meaglin.wasabi@gmail.com>",
"Mike Kusold <hello@mikekusold.com>",
"Alex Kit <alex.kit@atmajs.com>",
"Santiago Gimeno <santiago.gimeno@gmail.com>",
"Daniel <darc.tec@gmail.com>",
"Christian Steininger <christian.steininger.cs@gmail.com>",
"Mykola Piskovyi <m.piskovyi@gmail.com>",
"Brian Vaughn <brian.david.vaughn@gmail.com>",
"Nicholas Clawson <nickclaw@gmail.com>",
"Yasuhiroki <yasuhiroki.duck@gmail.com>",
"Nicholas Clawson <nickclaw@gmail.com>",
"Brendan Warkentin <faazshift@gmail.com>",
"Charlie Fish <fishcharlie.code@gmail.com>",
"Ian Graves <ian+diskimage@iangrav.es>",
"Andy Thompson <me@andytson.com>",
"Regev Brody <regevbr@gmail.com>"
],
"license": "MIT",
"dependencies": {
"luxon": "^3.2.1"
},
"devDependencies": {
"eslint": "^8.27.0",
"sinon": "^15.0.1",
"tap": "^16.3.3",
"tsd": "^0.26.0"
},
"engines": {
"node": ">=12.0.0"
},
"browser": {
"fs": false
},
"tap": {
"check-coverage": false
},
"tsd": {
"directory": "test",
"compilerOptions": {
"lib": [
"es2017",
"dom"
]
}
},
"files": [
"lib",
"types",
"LICENSE",
"README.md"
]
}
+131
View File
@@ -0,0 +1,131 @@
export type DateType = Date | number | string
export interface CronDate {
addYear(): void
addMonth(): void
addDay(): void
addHour(): void
addMinute(): void
addSecond(): void
subtractYear(): void
subtractMonth(): void
subtractDay(): void
subtractHour(): void
subtractMinute(): void
subtractSecond(): void
getDate(): number
getFullYear(): number
getDay(): number
getMonth(): number
getHours(): number
getMinutes(): number
getSeconds(): number
getMilliseconds(): number
getTime(): number
getUTCDate(): number
getUTCFullYear(): number
getUTCDay(): number
getUTCMonth(): number
getUTCHours(): number
getUTCMinutes(): number
getUTCSeconds(): number
toISOString(): string
toJSON(): string
setDate(d: number): void
setFullYear(y: number): void
setDay(d: number): void
setMonth(m: number): void
setHours(h: number): void
setMinutes(m: number): void
setSeconds(s: number): void
setMilliseconds(s: number): void
getTime(): number
toString(): string
toDate(): Date
isLastDayOfMonth(): boolean
}
export interface ParserOptions<IsIterable extends boolean = false> {
currentDate?: DateType
startDate?: DateType
endDate?: DateType
utc?: boolean
tz?: string
nthDayOfWeek?: number
iterator?: IsIterable
}
type IteratorResultOrCronDate<IsIterable extends boolean> = IsIterable extends true
? IteratorResult<CronDate, CronDate>
: CronDate;
export interface ICronExpression<CronFields, IsIterable extends boolean> {
readonly fields: CronFields;
/** Find next suitable date */
next(): IteratorResultOrCronDate<IsIterable>
/** Find previous suitable date */
prev(): IteratorResultOrCronDate<IsIterable>
/** Check if next suitable date exists */
hasNext(): boolean
/** Check if previous suitable date exists */
hasPrev(): boolean
/** Iterate over expression iterator */
iterate(steps: number, callback?: (item: IteratorResultOrCronDate<IsIterable>, i: number) => void): IteratorResultOrCronDate<IsIterable>[]
/** Reset expression iterator state */
reset(resetDate?: string | number | Date): void
stringify(includeSeconds?: boolean): string
}
export interface IStringResult<CronFields> {
variables: Record<string, string>,
expressions: ICronExpression<CronFields, false>[],
errors: Record<string, any>,
}
+45
View File
@@ -0,0 +1,45 @@
import {
CronDate,
DateType,
ICronExpression,
IStringResult,
ParserOptions,
} from './common';
type BuildRangeTuple<Current extends [...number[]], Count extends number> =
Current["length"] extends Count
? Current
: BuildRangeTuple<[number, ...Current], Count>
type RangeTuple<Count extends number> = BuildRangeTuple<[], Count>
type BuildRange<Current extends number, End extends number, Accu extends [...number[]]> =
Accu["length"] extends End
? Current
: BuildRange<Current | Accu["length"], End, [number, ...Accu]>
type Range<StartInclusive extends number, EndExclusive extends number> = BuildRange<StartInclusive, EndExclusive, RangeTuple<StartInclusive>>
export type SixtyRange = Range<0, 30> | Range<30, 60>; // Typescript restriction on recursion depth
export type HourRange = Range<0, 24>;
export type DayOfTheMonthRange = Range<1, 32> | 'L';
export type MonthRange = Range<1, 13>;
export type DayOfTheWeekRange = Range<0, 8>;
export type CronFields = {
readonly second: readonly SixtyRange[];
readonly minute: readonly SixtyRange[];
readonly hour: readonly HourRange[];
readonly dayOfMonth: readonly DayOfTheMonthRange[];
readonly month: readonly MonthRange[];
readonly dayOfWeek: readonly DayOfTheWeekRange[];
}
export {ParserOptions, CronDate, DateType}
export type CronExpression<IsIterable extends boolean = false> = ICronExpression<CronFields, IsIterable>
export type StringResult = IStringResult<CronFields>
export function parseExpression<IsIterable extends boolean = false>(expression: string, options?: ParserOptions<IsIterable>): CronExpression<IsIterable>;
export function fieldsToExpression<IsIterable extends boolean = false>(fields: CronFields, options?: ParserOptions<IsIterable>): CronExpression<IsIterable>;
export function parseFile(filePath: string, callback: (err: any, data: StringResult) => any): void;
export function parseString(data: string): StringResult;
+28
View File
@@ -0,0 +1,28 @@
import {
CronDate,
DateType,
ICronExpression,
IStringResult,
ParserOptions,
} from '../common';
export type CronFields = {
readonly second: readonly number[];
readonly minute: readonly number[];
readonly hour: readonly number[];
readonly dayOfMonth: readonly (number | 'L')[];
readonly month: readonly number[];
readonly dayOfWeek: readonly number[];
}
export {ParserOptions, CronDate, DateType}
export type CronExpression<IsIterable extends boolean = false> = ICronExpression<CronFields, IsIterable>
export type StringResult = IStringResult<CronFields>
export function parseExpression<IsIterable extends boolean = false>(expression: string, options?: ParserOptions<IsIterable>): CronExpression<IsIterable>;
export function fieldsToExpression<IsIterable extends boolean = false>(fields: CronFields, options?: ParserOptions<IsIterable>): CronExpression<IsIterable>;
export function parseFile(filePath: string, callback: (err: any, data: StringResult) => any): void;
export function parseString(data: string): StringResult;