2026-08-18 16:21:22 +00:00
|
|
|
export type Subscription = {
|
|
|
|
|
id: string
|
|
|
|
|
key: string
|
|
|
|
|
pubkey: string
|
|
|
|
|
email: string
|
|
|
|
|
frequency: string
|
feat: allow users to choose digest schedule (hour, minute, dayOfWeek, timezone)
Add DB migration (hour, minute, day_of_week, timezone columns) with
idempotent ALTER TABLE upgrade path for existing databases.
Extend getCronExpression with optional dayOfWeek parameter; weekly
defaults to Monday (1) when not specified.
Worker createJob now passes stored schedule fields to cron expression
and uses the subscription's IANA timezone instead of hardcoded 'UTC'.
PUT /subscription/email accepts optional hour (0-23), minute (0-59),
dayOfWeek (1-7, only for weekly), timezone (IANA). GET response
includes the new fields. Omitted fields fall back to defaults (17:00
UTC).
Closes mailship-200
2026-09-23 14:50:04 +00:00
|
|
|
hour: number
|
|
|
|
|
minute: number
|
|
|
|
|
day_of_week?: number
|
|
|
|
|
timezone: string
|
2026-08-18 16:21:22 +00:00
|
|
|
created_at: number
|
|
|
|
|
confirmed_at?: number
|
|
|
|
|
unsubscribed_at?: number
|
|
|
|
|
last_digest_at?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const getSubscriptionError = (sub: Subscription) => {
|
|
|
|
|
if (!sub.email?.includes('@')) {
|
|
|
|
|
return 'Please provide a valid email address'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!['daily', 'weekly'].includes(sub.frequency)) {
|
|
|
|
|
return 'Frequency must be "daily" or "weekly"'
|
|
|
|
|
}
|
|
|
|
|
|
feat: allow users to choose digest schedule (hour, minute, dayOfWeek, timezone)
Add DB migration (hour, minute, day_of_week, timezone columns) with
idempotent ALTER TABLE upgrade path for existing databases.
Extend getCronExpression with optional dayOfWeek parameter; weekly
defaults to Monday (1) when not specified.
Worker createJob now passes stored schedule fields to cron expression
and uses the subscription's IANA timezone instead of hardcoded 'UTC'.
PUT /subscription/email accepts optional hour (0-23), minute (0-59),
dayOfWeek (1-7, only for weekly), timezone (IANA). GET response
includes the new fields. Omitted fields fall back to defaults (17:00
UTC).
Closes mailship-200
2026-09-23 14:50:04 +00:00
|
|
|
if (sub.hour < 0 || sub.hour > 23 || !Number.isInteger(sub.hour)) {
|
|
|
|
|
return 'Hour must be an integer between 0 and 23'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sub.minute < 0 || sub.minute > 59 || !Number.isInteger(sub.minute)) {
|
|
|
|
|
return 'Minute must be an integer between 0 and 59'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sub.frequency === 'weekly') {
|
|
|
|
|
if (sub.day_of_week === undefined || sub.day_of_week === null) {
|
|
|
|
|
return 'dayOfWeek is required for weekly frequency'
|
|
|
|
|
}
|
|
|
|
|
const dow = sub.day_of_week
|
|
|
|
|
if (dow < 0 || dow > 7 || !Number.isInteger(dow)) {
|
|
|
|
|
return 'dayOfWeek must be an integer between 0 and 7 (1=Mon, 7=Sun, 0=Sun)'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!sub.timezone || typeof sub.timezone !== 'string') {
|
|
|
|
|
return 'A valid IANA timezone is required'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
Intl.DateTimeFormat(undefined, { timeZone: sub.timezone })
|
|
|
|
|
} catch {
|
|
|
|
|
return `"${sub.timezone}" is not a valid IANA timezone`
|
|
|
|
|
}
|
2026-08-18 16:21:22 +00:00
|
|
|
}
|
|
|
|
|
|
feat: allow users to choose digest schedule (hour, minute, dayOfWeek, timezone)
Add DB migration (hour, minute, day_of_week, timezone columns) with
idempotent ALTER TABLE upgrade path for existing databases.
Extend getCronExpression with optional dayOfWeek parameter; weekly
defaults to Monday (1) when not specified.
Worker createJob now passes stored schedule fields to cron expression
and uses the subscription's IANA timezone instead of hardcoded 'UTC'.
PUT /subscription/email accepts optional hour (0-23), minute (0-59),
dayOfWeek (1-7, only for weekly), timezone (IANA). GET response
includes the new fields. Omitted fields fall back to defaults (17:00
UTC).
Closes mailship-200
2026-09-23 14:50:04 +00:00
|
|
|
export const getCronExpression = (frequency: string, hour = 17, minute = 0, dayOfWeek?: number) => {
|
2026-08-18 16:21:22 +00:00
|
|
|
if (frequency === 'daily') {
|
|
|
|
|
return `0 ${minute} ${hour} * * *`
|
|
|
|
|
}
|
feat: allow users to choose digest schedule (hour, minute, dayOfWeek, timezone)
Add DB migration (hour, minute, day_of_week, timezone columns) with
idempotent ALTER TABLE upgrade path for existing databases.
Extend getCronExpression with optional dayOfWeek parameter; weekly
defaults to Monday (1) when not specified.
Worker createJob now passes stored schedule fields to cron expression
and uses the subscription's IANA timezone instead of hardcoded 'UTC'.
PUT /subscription/email accepts optional hour (0-23), minute (0-59),
dayOfWeek (1-7, only for weekly), timezone (IANA). GET response
includes the new fields. Omitted fields fall back to defaults (17:00
UTC).
Closes mailship-200
2026-09-23 14:50:04 +00:00
|
|
|
// Weekly: default to Monday (1) when not specified
|
|
|
|
|
return `0 ${minute} ${hour} * * ${dayOfWeek ?? 1}`
|
2026-08-18 16:21:22 +00:00
|
|
|
}
|