mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 16:40:56 +00:00
55 lines
2.3 KiB
TypeScript
55 lines
2.3 KiB
TypeScript
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;
|
|
}
|