import { BadRequestException } from "@nestjs/common"; /** * Keyset cursor for paging a thread backwards from newest. * * The cursor is just a message id. The sort key is the pair `(created_at, id)` — * two messages can share a timestamp, and a cursor on a non-unique key either * re-serves or skips the tied rows — but the *timestamp half is never sent over * the wire*, because it cannot survive the trip. * * `support_messages.created_at` is `timestamptz(6)`; a JS `Date` holds only * milliseconds, so the value TypeORM hands back is already truncated. Encoding * that into the cursor and comparing against it would silently skip every row * sharing the cursor's millisecond but earlier within it (`.254100` is not * `< .254000`) — those rows would never appear on any page. Sending the id alone * and letting Postgres look the real `(created_at, id)` up keeps the comparison * at full precision on the server, where it was never lossy. * * Opaque on purpose (base64): clients must treat it as a token, so the sort key * can change without a contract change. * * The passenger API's twin encodes a timestamp because its column is * `TIMESTAMP(3)` — millisecond, matching JS exactly — so it has no such loss. * The two formats are deliberately NOT interchangeable; each app reads only its * own cursors. */ export function encodeMessageCursor(id: string): string { return Buffer.from(id, "utf8").toString("base64url"); } /** * Parse a client-supplied cursor. Rejects anything malformed rather than * silently falling back to "first page" — a corrupted cursor that degrades to * page 1 makes an infinite scroll loop forever over the same rows. */ export function decodeMessageCursor(raw: string): string { let id: string; try { id = Buffer.from(raw, "base64url").toString("utf8"); } catch { throw new BadRequestException("Malformed pagination cursor."); } // The id goes into a parameterized query, but validate the shape anyway: a // non-uuid can only be a mangled cursor, and failing loudly here beats an // empty page that reads as "start of conversation". if ( !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) ) { throw new BadRequestException("Malformed pagination cursor."); } return id; }