Skip to content

play_date is served as raw Postgres timestamp text, not the RFC 3339 api.yaml declares #2349

Description

@jakebromberg

GET /flowsheet/search returns play_date as raw PostgreSQL timestamp text — "2019-03-01 12:00:00+00" — where api.yaml declares it type: string, format: date-time (RFC 3339). The served value is not RFC 3339: it uses a space instead of T, and a two-digit +00 offset instead of +00:00 or Z.

Pre-existing and long-standing; clients cope because Date.parse in V8 is lenient about both deviations. A stricter parser — Swift's ISO8601DateFormatter, Python's datetime.fromisoformat before 3.11, most Go and Rust RFC 3339 parsers — rejects it.

Cause

apps/backend/services/search.service.ts declares the row type as play_date: Date and transformRow branches on it:

play_date: row.play_date instanceof Date ? row.play_date.toISOString() : String(row.play_date ?? ''),

The instanceof Date branch is dead. drizzle-orm's postgres-js driver installs an identity parser over the timestamp OIDs at construction — node_modules/drizzle-orm/postgres-js/driver.cjs:

const transparentParser = (val) => val;
for (const type of ["1184", "1082", "1083", "1114", "1182", "1185", "1115", "1231"]) {
  client.options.parsers[type] = transparentParser;
  ...
}

OID 1184 is timestamptz. So a raw db.execute never yields a Date, the String(...) branch always runs, and Postgres's own text rendering reaches the client verbatim.

The Date type annotation is therefore a type lie that has been masking this, and it also means the unit tier — whose mocks supply real Date objects — exercises a shape production never produces.

Scope

The observed instance is /flowsheet/search. The cause is not specific to it: any endpoint that reads a timestamptz through a raw db.execute and serializes it directly has the same behaviour. Part of the work is establishing how wide that is. Endpoints using the drizzle query builder with typed columns are not necessarily affected in the same way; that should be checked rather than assumed.

Desired end state

play_date — and any other format: date-time field served from a raw db.execute — is RFC 3339, matching what api.yaml promises, without a per-call-site new Date(...) round trip that would reintroduce millisecond truncation.

Suggested approach

Render the timestamp in SQL, the way #2346 now does for the cursor:

to_char(add_time AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS play_date

This is exact, avoids the JS Date millisecond floor, and keeps the response contract honest.

Whatever the fix, correct the SearchResultRow.play_date type so it describes what the driver actually returns, and make at least one unit mock supply the real shape — the current mocks supplying Date are why this was invisible.

Constraints

  • This is a response-shape change on a live endpoint. Existing consumers parse the current form successfully; the new form is strictly more standard, but the change should be checked against dj-site and any other consumer before shipping.
  • Per WXYC/wiki's api.yaml consumer fan-out notes, iOS uses operation-derived types while dj-site uses schema types, so a contract-adjacent change needs both checked.
  • Do not fix this by parsing to a JS Date and calling .toISOString()Date is millisecond-precision and would silently floor the microseconds that Postgres returns.

Acceptance criteria

  • play_date validates against format: date-time (RFC 3339)
  • Microsecond precision is preserved, not floored
  • SearchResultRow's type matches the driver's actual return shape, and a unit test exercises that shape
  • An audit records which other endpoints serve timestamptz through raw db.execute, with tickets or fixes for any that are also non-conformant
  • Consumers verified before merge

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsearchLibrary search and Elasticsearch

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions