Utilities for working with cron expressions, timezones, and scheduled tasks.
This module provides the following functionality:
/**
* Get the timezone offset difference in minutes between two timezones
* @param oldTz - Original timezone
* @param newTz - New timezone
* @returns Offset in minutes
*/
function getTzMinutesOffset(oldTz: string, newTz: string): number;
/**
* Format minutes offset as human-readable string
* @param offsetMins - Offset in minutes
* @returns Formatted offset string (e.g., "+05:30", "-08:00")
*/
function formatMinutesOffset(offsetMins: number): string;
/**
* Get human-readable timezone label with UTC offset
* @param timezone - IANA timezone name (e.g., "America/New_York")
* @returns Formatted label (e.g., "(UTC -05:00) America/New York")
*/
function getTimezoneLabel(timezone: string | undefined): string | undefined;
/**
* Convert cron expression to human-readable format
* @param cronExpression - Cron expression string
* @param timezone - IANA timezone name
* @returns Human-readable description
*/
function getHumanReadableCronExpression(
cronExpression: string,
timezone: string
): string;
/**
* Validate that cron frequency is at least 1 hour
* @param cronExpression - Cron expression to validate
* @returns true if frequency is >= 1 hour
*/
function isValidFrequency(cronExpression: string): boolean;
/**
* Validate timezone string
* @param timezone - IANA timezone name to validate
* @returns true if timezone is valid
*/
function isValidTimezone(timezone: string | undefined): boolean;import {
getTzMinutesOffset,
formatMinutesOffset,
getTimezoneLabel,
isValidTimezone,
} from '@lightdash/common';
// Get timezone offset difference
const offset = getTzMinutesOffset('America/New_York', 'America/Los_Angeles');
// Returns: -180 (3 hours difference)
// Format offset as string
const formatted = formatMinutesOffset(-300);
// Returns: "-05:00"
const formatted2 = formatMinutesOffset(330);
// Returns: "+05:30"
// Get timezone label with offset
const label = getTimezoneLabel('America/Los_Angeles');
// Returns: "(UTC -08:00) America/Los Angeles"
const label2 = getTimezoneLabel('Asia/Kolkata');
// Returns: "(UTC +05:30) Asia/Kolkata"
// Validate timezone
if (isValidTimezone('UTC')) {
console.log('Valid timezone');
}
console.log(isValidTimezone('Invalid/Timezone')); // false
console.log(isValidTimezone('America/New_York')); // trueimport {
getHumanReadableCronExpression,
isValidFrequency,
} from '@lightdash/common';
// Convert cron to human-readable format
const readable = getHumanReadableCronExpression('0 9 * * *', 'America/New_York');
// Returns: "at 09:00 AM (UTC -05:00)"
const readable2 = getHumanReadableCronExpression('0 */2 * * *', 'UTC');
// Returns: "every 2 hours (UTC +00:00)"
const readable3 = getHumanReadableCronExpression('0 0 * * 1', 'Europe/London');
// Returns: "at 12:00 AM on Monday (UTC +00:00)"
// Validate cron frequency (must be >= 1 hour)
if (isValidFrequency('0 * * * *')) {
console.log('Valid - runs hourly');
}
console.log(isValidFrequency('0 */2 * * *')); // true - every 2 hours
console.log(isValidFrequency('*/30 * * * *')); // false - every 30 minutes
console.log(isValidFrequency('0 0 * * *')); // true - dailyimport {
getHumanReadableCronExpression,
isValidFrequency,
isValidTimezone,
getTimezoneLabel,
} from '@lightdash/common';
interface SchedulerConfig {
cronExpression: string;
timezone: string;
}
function validateSchedulerConfig(config: SchedulerConfig): string[] {
const errors: string[] = [];
// Validate timezone
if (!isValidTimezone(config.timezone)) {
errors.push('Invalid timezone');
}
// Validate frequency
if (!isValidFrequency(config.cronExpression)) {
errors.push('Schedule frequency must be at least 1 hour');
}
return errors;
}
function getSchedulerDescription(config: SchedulerConfig): string {
return getHumanReadableCronExpression(
config.cronExpression,
config.timezone
);
}
// Usage
const config: SchedulerConfig = {
cronExpression: '0 9 * * *',
timezone: 'America/New_York',
};
const errors = validateSchedulerConfig(config);
if (errors.length === 0) {
const description = getSchedulerDescription(config);
console.log(`Schedule: ${description}`);
// Output: "Schedule: at 09:00 AM (UTC -05:00)"
}import {
getHumanReadableCronExpression,
getTimezoneLabel,
isValidFrequency,
} from '@lightdash/common';
interface DashboardSchedule {
dashboardId: string;
cronExpression: string;
timezone: string;
recipients: string[];
}
async function createDashboardSchedule(
schedule: DashboardSchedule
): Promise<void> {
// Validate frequency
if (!isValidFrequency(schedule.cronExpression)) {
throw new Error('Schedule must run at least once per hour');
}
// Get human-readable description
const description = getHumanReadableCronExpression(
schedule.cronExpression,
schedule.timezone
);
console.log(`Creating schedule: ${description}`);
await database.schedules.create({
...schedule,
description,
});
}
// Usage
await createDashboardSchedule({
dashboardId: 'dash-123',
cronExpression: '0 8 * * 1-5', // Weekdays at 8 AM
timezone: 'America/New_York',
recipients: ['user@example.com'],
});import { getTimezoneLabel, isValidTimezone } from '@lightdash/common';
// Get list of common timezones with labels
const commonTimezones = [
'UTC',
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'Europe/London',
'Europe/Paris',
'Asia/Tokyo',
'Asia/Shanghai',
'Australia/Sydney',
];
function getTimezoneOptions() {
return commonTimezones
.filter(isValidTimezone)
.map(tz => ({
value: tz,
label: getTimezoneLabel(tz),
}));
}
// Usage in React component
function TimezoneSelector({ value, onChange }: Props) {
const options = getTimezoneOptions();
return (
<select value={value} onChange={(e) => onChange(e.target.value)}>
{options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
}import { getTzMinutesOffset, formatMinutesOffset } from '@lightdash/common';
async function migrateScheduleTimezone(
scheduleId: string,
newTimezone: string
) {
const schedule = await getSchedule(scheduleId);
// Calculate time offset
const offsetMinutes = getTzMinutesOffset(
schedule.timezone,
newTimezone
);
const offsetFormatted = formatMinutesOffset(offsetMinutes);
console.log(`Migrating schedule from ${schedule.timezone} to ${newTimezone}`);
console.log(`Time will shift by ${offsetFormatted}`);
// Update schedule
await database.schedules.update(scheduleId, {
timezone: newTimezone,
// Note: Cron expression stays the same, but will execute at different UTC time
});
}import {
getHumanReadableCronExpression,
isValidFrequency,
isValidTimezone,
} from '@lightdash/common';
// Create schedule endpoint
app.post('/api/schedules', async (req, res) => {
const { cronExpression, timezone, dashboardId } = req.body;
// Validate timezone
if (!isValidTimezone(timezone)) {
return res.status(400).json({
error: 'Invalid timezone',
});
}
// Validate frequency
if (!isValidFrequency(cronExpression)) {
return res.status(400).json({
error: 'Schedule frequency must be at least 1 hour',
});
}
// Get human-readable description
const description = getHumanReadableCronExpression(
cronExpression,
timezone
);
const schedule = await createSchedule({
cronExpression,
timezone,
dashboardId,
description,
});
res.json(schedule);
});
// Get timezone list endpoint
app.get('/api/timezones', (req, res) => {
const timezones = Intl.supportedValuesOf('timeZone')
.filter(isValidTimezone)
.map(tz => ({
value: tz,
label: getTimezoneLabel(tz),
}));
res.json(timezones);
});import { getHumanReadableCronExpression } from '@lightdash/common';
// External dependency - Install separately: npm install cronstrue
import cronstrue from 'cronstrue';
function getSchedulePreview(
cronExpression: string,
timezone: string
): {
readable: string;
nextRuns: Date[];
} {
// Get human-readable description
const readable = getHumanReadableCronExpression(cronExpression, timezone);
// Calculate next run times (using a cron library)
const parser = require('cron-parser');
const interval = parser.parseExpression(cronExpression, { tz: timezone });
const nextRuns: Date[] = [];
for (let i = 0; i < 5; i++) {
nextRuns.push(interval.next().toDate());
}
return { readable, nextRuns };
}
// Usage
const preview = getSchedulePreview('0 9 * * 1-5', 'America/New_York');
console.log(`Schedule: ${preview.readable}`);
console.log('Next runs:', preview.nextRuns);import {
getTzMinutesOffset,
formatMinutesOffset,
getTimezoneLabel,
isValidFrequency,
isValidTimezone,
getHumanReadableCronExpression,
} from '@lightdash/common';
describe('Scheduler utilities', () => {
describe('timezone utilities', () => {
it('should calculate timezone offset', () => {
const offset = getTzMinutesOffset('UTC', 'America/New_York');
expect(offset).toBeLessThan(0); // NY is behind UTC
});
it('should format offset', () => {
expect(formatMinutesOffset(-300)).toBe('-05:00');
expect(formatMinutesOffset(330)).toBe('+05:30');
});
it('should get timezone label', () => {
const label = getTimezoneLabel('UTC');
expect(label).toContain('UTC');
});
it('should validate timezone', () => {
expect(isValidTimezone('UTC')).toBe(true);
expect(isValidTimezone('Invalid/Zone')).toBe(false);
});
});
describe('cron utilities', () => {
it('should validate frequency', () => {
expect(isValidFrequency('0 * * * *')).toBe(true); // Hourly
expect(isValidFrequency('*/30 * * * *')).toBe(false); // Every 30 min
});
it('should get human readable cron', () => {
const readable = getHumanReadableCronExpression('0 9 * * *', 'UTC');
expect(readable).toContain('09:00');
});
});
});| Expression | Description | Valid? |
|---|---|---|
0 * * * * | Every hour | ✓ |
0 */2 * * * | Every 2 hours | ✓ |
0 9 * * * | Daily at 9 AM | ✓ |
0 9 * * 1-5 | Weekdays at 9 AM | ✓ |
0 0 * * 0 | Weekly on Sunday | ✓ |
*/30 * * * * | Every 30 minutes | ✗ (too frequent) |
*/15 * * * * | Every 15 minutes | ✗ (too frequent) |