import { ActionSchedule } from '@prisma/client' import { DAY_NAMES, numberWithOrdinal, timeToDisplayString } from './date' import { TimeZone } from './timezones' export interface CronSchedule { second: string minute: string hour: string dayOfMonth: string month: string dayOfWeek: string timeZoneName: string // Also a TimeZone, broad for deserializing convenience runnerId?: string | null notifyOnSuccess?: boolean } export function cronSchedulesEqual(a: CronSchedule, b: CronSchedule) { for (const propName of [ 'second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek', 'timeZoneName', 'runnerId', ]) { if (a[propName] !== b[propName]) return false } return true } export function cronScheduleToString(schedule: CronSchedule): string { return [ schedule.second, schedule.minute, schedule.hour, schedule.dayOfMonth, schedule.month, schedule.dayOfWeek, ].join(' ') } export type SchedulePeriod = 'hour' | 'day' | 'week' | 'month' export interface ScheduleInput { schedulePeriod: SchedulePeriod timeZoneName?: TimeZone hours?: number minutes?: number dayOfWeek?: number dayOfMonth?: number runnerId?: string | null notifyOnSuccess?: boolean } export function actionScheduleToDescriptiveString( schedule: ActionSchedule ): string { const { minute, hour, dayOfMonth, dayOfWeek, timeZoneName } = schedule const timeString = timeToDisplayString( Number(hour), Number(minute), timeZoneName ) if (dayOfMonth !== '*') { return `every month on the ${numberWithOrdinal( Number(dayOfMonth) )} at ${timeString}` } if (dayOfWeek !== '*') { return `every week on ${DAY_NAMES[dayOfWeek]} at ${timeString}` } if (hour !== '*') { return `every day at ${timeString}` } return `every hour` } /** * This assumes well-formed simple cron schedules generated by toCronSchedule below. */ export function toScheduleInput(schedule: CronSchedule): ScheduleInput { const hours = Number(schedule.hour) const minutes = Number(schedule.minute) const dayOfWeek = Number(schedule.dayOfWeek) const dayOfMonth = Number(schedule.dayOfMonth) const schedulePeriod: SchedulePeriod = !Number.isNaN(dayOfMonth) ? 'month' : !Number.isNaN(dayOfWeek) ? 'week' : !Number.isNaN(hours) ? 'day' : 'hour' const common = { schedulePeriod, timeZoneName: schedule.timeZoneName as TimeZone, runnerId: schedule.runnerId, notifyOnSuccess: schedule.notifyOnSuccess, } switch (schedulePeriod) { case 'hour': return common case 'day': return { ...common, minutes, hours, } case 'week': return { ...common, minutes, hours, dayOfWeek, } case 'month': return { ...common, minutes, hours, dayOfMonth, } } } export function toCronSchedule({ schedulePeriod, timeZoneName = 'UTC', hours, minutes, dayOfWeek, dayOfMonth, runnerId, notifyOnSuccess, }: ScheduleInput): CronSchedule | undefined { switch (schedulePeriod) { case undefined: return undefined case 'hour': { return { second: '0', minute: '0', hour: '*', dayOfMonth: '*', month: '*', dayOfWeek: '*', timeZoneName, runnerId, notifyOnSuccess, } } case 'day': { return { second: '0', minute: minutes?.toString() ?? '0', hour: hours?.toString() ?? '0', dayOfMonth: '*', month: '*', dayOfWeek: '*', timeZoneName, runnerId, notifyOnSuccess, } } case 'week': { return { second: '0', minute: minutes?.toString() ?? '0', hour: hours?.toString() ?? '0', dayOfMonth: '*', month: '*', dayOfWeek: dayOfWeek?.toString() ?? '0', timeZoneName, runnerId, notifyOnSuccess, } } case 'month': { return { second: '0', minute: minutes?.toString() ?? '0', hour: hours?.toString() ?? '0', dayOfMonth: dayOfMonth?.toString() ?? '0', month: '*', dayOfWeek: '*', timeZoneName, runnerId, notifyOnSuccess, } } } }