Skip to content

core: handle invalid input in formatDate without leaking 'Invalid Date'#9848

Open
calebcgates wants to merge 1 commit into
streetwriters:masterfrom
calebcgates:fix/core-formatdate-handle-invalid-input
Open

core: handle invalid input in formatDate without leaking 'Invalid Date'#9848
calebcgates wants to merge 1 commit into
streetwriters:masterfrom
calebcgates:fix/core-formatdate-handle-invalid-input

Conversation

@calebcgates
Copy link
Copy Markdown

Description

packages/core/src/utils/date.ts:formatDate(date, options) declares its signature as:

export function formatDate(
  date: string | number | Date | null | undefined,
  options: FormatDateOptions = { ... }
)

The signature explicitly accepts null and undefined, but the body doesn't honor that contract — it passes the argument straight into dayjs(date).format(...). The result depends on which "invalid" value the caller passes:

  • dayjs(null).format(...) → literal string "Invalid Date"
  • dayjs(undefined).format(...)today's date (because dayjs(undefined) === dayjs(), which returns "now")
  • dayjs("").format(...)"Invalid Date"
  • dayjs("not-a-date").format(...)"Invalid Date"

So today, calling formatDate(null) returns "Invalid Date", and formatDate(undefined) silently returns a fresh timestamp for today — neither of which matches the implicit contract suggested by accepting those types in the signature.

Most callers in the repo are defensively guarded already (note.expiryDate?.value && formatDate(...), apiKey.lastUsedAt ? formatDate(...) : "Never used", etc.), and the upstream TypeScript types claim most date fields are non-nullable number. So this isn't a user-reported bug — it's a contract-honesty fix. The function says it accepts null | undefined; this PR makes it actually do something sensible when it gets them.

Solution

Two guards at the top of formatDate:

  1. An explicit null/undefined short-circuit returning "". Needed because dayjs(undefined) is the no-arg constructor, which returns the current time and reports .isValid() === true — so a single .isValid() check is not enough to cover undefined.
  2. A dayjs(date).isValid() check returning "" for unparseable strings, empty strings, and NaN.

The fix mirrors the .isValid() pattern already used in this package at packages/core/src/utils/query-transformer.ts:116 (return date?.isValid() ? date.toDate().getTime() : null;).

The dayjs(date) construction is hoisted to a const d and reused across all six switch branches — small side-effect win, also keeps the diff visually localised.

Empty string "" is chosen as the sentinel because every existing caller consumes the return value directly into a template literal or a React text prop, both of which render "" invisibly. Returning null would change the type from string to string | null and break ~12 call sites in 4 files.

No signature change. No new imports. No new dependencies. Valid inputs produce byte-identical output.

Type of Change

  • Bug fix
  • Feature

Visuals

  • Attached relevant screenshots / screen recording / GIF
  • N/A (not a feature or no UI changes)

Testing

  • Ran all E2E tests
  • Ran all integration tests
  • Added/updated tests for this change (if needed)
  • N/A (tests not needed — explanation provided below)

If tests were not added, explain why

Tests were added at packages/core/src/utils/__tests__/date.test.ts covering:

  1. Valid numeric epoch → date-time format (round-trip)
  2. Valid Date instance → date format
  3. Valid ISO string → time format
  4. null input → ""
  5. undefined input → "" (regression guard for the dayjs(undefined) === dayjs() idiosyncrasy)
  6. Unparseable string → ""
  7. Invalid input regardless of options.type (date, time, timezone)
  8. Regression guard: output never contains the substring "Invalid"

Run with npm run test:core (or npx vitest run src/utils/__tests__/date.test.ts from inside packages/core). All 8 new tests pass; full packages/core/src/utils/__tests__/ suite passes 99/99 (no regression to fuzzy, query-transformer, content-block, internal-link, set, html-diff, title-format, virtualized-grouping).

Platform

  • Web
  • Mobile
  • Desktop

(@notesnook/core ships in all three apps; this fix takes effect everywhere formatDate is called.)

Sign-off

  • QA passed
  • UI/UX passed

Signed-off-by: Caleb Gates <calebcgates@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant