Add timezone support and safe date formatting utilities#31
Conversation
The root cause was Drizzle ORM's `timestamp()` (without timezone) using a
fragile date conversion: `new Date(value + "+0000")` which produces
non-standard date strings that can result in Invalid Date objects depending
on the runtime environment.
Two-part fix:
1. Schema: Change all `timestamp()` columns to `timestamp({ withTimezone: true })`
so Drizzle uses `new Date(value)` directly on timezone-aware PostgreSQL
strings. Includes migration 0009 to convert existing columns.
2. Defense: Add `safeFormatISO` / `safeToISOString` utilities that validate
dates before formatting, preventing RangeError crashes in `formatISO` and
`.toISOString()` call sites across formatting, extract-graph, dream, and
ingest-conversation.
https://claude.ai/code/session_01SDE1HZqgcEgcjWs1UPqAY6
Replace manually written migration with proper drizzle-kit generate output, which includes the snapshot JSON needed for drizzle-kit migrate to work. https://claude.ai/code/session_01SDE1HZqgcEgcjWs1UPqAY6
There was a problem hiding this comment.
Code Review
This pull request migrates database timestamp columns to timestamptz and introduces a new safe-date utility to handle invalid date values gracefully across the application. The review feedback suggests refactoring the internal toValidDate helper into an exported safeToDate function to centralize validation logic and eliminate manual date checks in conversation-store.ts, improving maintainability and consistency.
| import { formatISO } from "date-fns"; | ||
|
|
||
| /** | ||
| * Converts a value to a valid Date, returning the current time if invalid. | ||
| */ | ||
| function toValidDate(value: Date | string | number): Date { | ||
| const date = value instanceof Date ? value : new Date(value); | ||
| if (isNaN(date.getTime())) { | ||
| return new Date(); | ||
| } | ||
| return date; | ||
| } | ||
|
|
||
| /** | ||
| * Safely formats a date value as ISO 8601. | ||
| * Falls back to the current time if the input is an invalid date, | ||
| * preventing RangeError: Invalid time value crashes. | ||
| */ | ||
| export function safeFormatISO(value: Date | string | number): string { | ||
| return formatISO(toValidDate(value)); | ||
| } | ||
|
|
||
| /** | ||
| * Safely converts a date value to an ISO string. | ||
| * Falls back to the current time if the input is an invalid date. | ||
| */ | ||
| export function safeToISOString(value: Date | string | number): string { | ||
| return toValidDate(value).toISOString(); | ||
| } |
There was a problem hiding this comment.
The internal helper function toValidDate is highly reusable. I recommend renaming it to safeToDate to match the naming convention of the other exports in this module and exporting it so it can be used in conversation-store.ts and other parts of the codebase. This avoids duplicating validation logic and the overhead of creating multiple Date objects.
import { formatISO } from "date-fns";
/**
* Converts a value to a valid Date, returning the current time if invalid.
*/
export function safeToDate(value: Date | string | number): Date {
const date = value instanceof Date ? value : new Date(value);
if (isNaN(date.getTime())) {
return new Date();
}
return date;
}
/**
* Safely formats a date value as ISO 8601.
* Falls back to the current time if the input is an invalid date,
* preventing RangeError: Invalid time value crashes.
*/
export function safeFormatISO(value: Date | string | number): string {
return formatISO(safeToDate(value));
}
/**
* Safely converts a date value to an ISO string.
* Falls back to the current time if the input is an invalid date.
*/
export function safeToISOString(value: Date | string | number): string {
return safeToDate(value).toISOString();
}| timestamp: isNaN(new Date(meta.timestamp).getTime()) | ||
| ? new Date() | ||
| : new Date(meta.timestamp), |
There was a problem hiding this comment.
Instead of duplicating the validation logic and creating multiple Date objects here, use the new safeToDate utility (after exporting it from safe-date.ts). This improves maintainability and ensures consistent fallback behavior across the application. Note: You will need to add the corresponding import for safeToDate.
timestamp: safeToDate(meta.timestamp),The manually-written 0009_timestamp_to_timestamptz.sql was replaced by the drizzle-kit generated 0009_married_smasher.sql but the deletion wasn't staged. https://claude.ai/code/session_01SDE1HZqgcEgcjWs1UPqAY6
- Export `safeToDate` from safe-date.ts (renamed from private `toValidDate`) per Gemini review: makes the helper reusable across the codebase - Use `safeToDate` in conversation-store.ts instead of inline validation, removing duplicated logic - Fix Prettier formatting in schema.ts, extract-graph.ts, dream.ts https://claude.ai/code/session_01SDE1HZqgcEgcjWs1UPqAY6
Summary
This PR adds comprehensive timezone support to the database schema and introduces safe date formatting utilities to prevent crashes from invalid date values.
Key Changes
Database Schema Updates
timestampcolumns totimestamp with time zone(timestamptz) across all tables (nodes, edges, embeddings, sources, user_profiles, scratchpads, etc.)0009_timestamp_to_timestamptz.sqlto safely convert existing timestamp columns to timestamptz, treating existing values as UTCNew Safe Date Utilities
src/lib/safe-date.tswith two exported functions:safeFormatISO(): Safely formats dates as ISO 8601, falling back to current time for invalid datessafeToISOString(): Safely converts dates to ISO strings with the same fallback behaviortoValidDate()helper that validates dates and returns current time if invalidUpdated Date Handling
src/lib/formatting.ts: Replaced directformatISO()calls withsafeFormatISO()for message, node, edge, and connection timestamp formattingsrc/lib/conversation-store.ts: Added inline validation when parsing conversation turn timestampssrc/lib/extract-graph.ts: Replaced.toISOString()withsafeToISOString()for node timestampssrc/lib/jobs/dream.ts: Replaced.toISOString()withsafeToISOString()for node timestampssrc/lib/jobs/ingest-conversation.ts: Replaced.toISOString()withsafeToISOString()for message timestampsImplementation Details
RangeError: Invalid time valuecrashes that can occur when formatting invalid dateshttps://claude.ai/code/session_01SDE1HZqgcEgcjWs1UPqAY6