feat(18-01): implement household timezone accessor + IANA validator
- getHouseholdTimezone(db): selects household_timezone from app_config - D-06 fallback chain: stored value → process.env.TZ → Intl.DateTimeFormat().resolvedOptions().timeZone - isValidIanaTimezone: try/catch Intl.DateTimeFormat (no Intl.supportedValuesOf per RESEARCH Pitfall 2) - Exports match D-05 single-accessor contract for reminderScheduler + outboxWorker - All 11 unit tests pass; 358/358 total suite green; tsc --noEmit clean
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* householdTimezone — shared accessor for the stored household timezone (D-05).
|
||||
*
|
||||
* D-05: single accessor so reminderScheduler.ts and outboxWorker.ts cannot drift.
|
||||
* D-06: fallback chain when household_timezone is unset:
|
||||
* stored value → process.env.TZ → Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
*
|
||||
* Do NOT use Intl.supportedValuesOf for IANA validation — it omits 'UTC' (RESEARCH Pitfall 2).
|
||||
* Use try/catch on Intl.DateTimeFormat construction instead.
|
||||
*/
|
||||
|
||||
import { eq } from 'drizzle-orm';
|
||||
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
||||
import type * as schema from '../db/schema.js';
|
||||
import { appConfig } from '../db/schema.js';
|
||||
|
||||
/**
|
||||
* Returns the stored household timezone from app_config, or falls back to:
|
||||
* 1. process.env.TZ (if set and non-empty)
|
||||
* 2. Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
*
|
||||
* This is the D-05 single source of truth for the server-side all-day "9 AM local"
|
||||
* reminder computation in reminderScheduler.ts and outboxWorker.ts.
|
||||
*/
|
||||
export async function getHouseholdTimezone(
|
||||
db: MySql2Database<typeof schema>,
|
||||
): Promise<string> {
|
||||
const [row] = await db
|
||||
.select({ value: appConfig.value })
|
||||
.from(appConfig)
|
||||
.where(eq(appConfig.key, 'household_timezone'))
|
||||
.limit(1);
|
||||
|
||||
return (
|
||||
row?.value ??
|
||||
process.env.TZ ??
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if `tz` is a valid IANA timezone identifier recognized by the
|
||||
* runtime's Intl implementation, false otherwise.
|
||||
*
|
||||
* Uses try/catch on Intl.DateTimeFormat construction — NOT Intl.supportedValuesOf,
|
||||
* which omits 'UTC' in some environments (RESEARCH Pitfall 2).
|
||||
*/
|
||||
export function isValidIanaTimezone(tz: string): boolean {
|
||||
try {
|
||||
Intl.DateTimeFormat(undefined, { timeZone: tz });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user