Files
twenty/packages/twenty-server/test
Charles Bochet dd7435b807 fix: normalize date-time field input on backend to prevent timeline crash (#22035)
## Context

Reported via support
([private-issues#477](https://github.com/twentyhq/private-issues/issues/477)):
a customer saw **"Invalid Configuration"** in red on a record's
**Timeline** tab. The dev console was flooded with:

```
RangeError: Cannot parse: 2026-05-07
    at Temporal.Instant.from (...)
    at RecordFieldComponent ...
```

## Root cause

A `DATE_TIME` field in their workspace holds **date-only** values like
`2026-05-07`.

`validateDateTimeFieldOrThrow` (the write-path validator) **accepts**
date-only formats — `'yyyy-MM-dd'` is in `ACCEPTED_DATE_TIME_FORMATS` —
and **returns the raw input string unchanged**, with no normalization.
So a date-only string passes validation and propagates verbatim into the
mutation response and the timeline event payload.

On render, `DateTimeDisplay` builds the timezone hint with
`Temporal.Instant.from(value)`. That's strict — it requires a full
instant (time + offset/`Z`) and throws `RangeError` on a bare date. The
throw escapes into the page-layout widget error boundary, which renders
the **"Invalid Configuration"** fallback and breaks the whole timeline.

## Fix

**Backend (root cause) — normalize on write.**
`validateDateTimeFieldOrThrow` now canonicalizes every accepted value to
a full ISO 8601 instant, so a date-only value can never reach storage,
the mutation response, or timeline events for a `DATE_TIME` field:

- strict ISO-8601 carrying an offset/`Z` -> kept as its exact instant
(server-timezone-independent)
- zoneless / date-only / lenient formats -> interpreted as **UTC**
(date-only -> midnight UTC), deterministically

Lenient input is preserved — parsing still uses date-fns for the ~20
accepted formats (which `Temporal.Instant.from` cannot parse); only the
*output* is canonicalized, via Temporal.

| input | before (stored raw) | after (normalized) |
|---|---|---|
| `2026-05-07` | `2026-05-07` | `2026-05-07T00:00:00Z` |
| `2026-05-07T12:00:00+02:00` | `2026-05-07T12:00:00+02:00` |
`2026-05-07T10:00:00Z` |
| `2026-05-07T12:00:00.000Z` | `2026-05-07T12:00:00.000Z` |
`2026-05-07T12:00:00Z` |
| `January 15, 2024` | `January 15, 2024` | `2024-01-15T00:00:00Z` |

**Frontend (existing data) — Temporal-native guard.** Existing
workspaces already have date-only values stored in events, so the
backend fix alone won't un-break the reporting customer's timeline.
`DateTimeDisplay` now parses the value via a new
`parseStringToInstantOrNull` helper (Temporal `Instant.from` with a
`PlainDate` start-of-day-UTC fallback) and only renders the timezone
hint when valid — so stored bad data renders gracefully instead of
crashing. This replaces the initial `new Date()` guard with a
Temporal-native one, in line with the codebase's Temporal migration.

## Tests

- `validate-date-time-field-or-throw.util.spec.ts` updated to assert the
normalized instant output, incl. explicit date-only -> midnight-UTC
cases.
- `parseStringToInstantOrNull.test.ts` — unit coverage for the frontend
helper (instant, offset, date-only, unparseable).
- `DateTimeDisplay.stories.tsx` — story rendering a date-only value
under a non-system timezone (the previously-crashing path).
2026-06-24 12:42:05 +00:00
..