fix: Make endless list pagination deterministic
The filtered branch of `getByEndlessListId` applied `LIMIT` without an `ORDER BY`, so Postgres was free to hand back any matching rows. With a search over 64 cards the first page returned the oldest ones and a full cursor walk reached 60 of them across 149 rows — cards both skipped and repeated. The query now orders the way the cursor reads it. The cursor itself is validated as ISO 8601, which admits forms Postgres rejects as a timestamp (`2026`, `2026-W35-3`, a comma as the decimal separator), each of them a 500 from the adapter. It is normalized before the query, and stays a string because the equality half of the cursor reads a `Date` as an empty constraint.
This commit is contained in:
@@ -243,8 +243,17 @@ module.exports = {
|
|||||||
filterLabelIds = filterLabelIds.filter((labelId) => availableLabelIdsSet.has(labelId));
|
filterLabelIds = filterLabelIds.filter((labelId) => availableLabelIdsSet.has(labelId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ISO 8601 allows forms Postgres rejects as a timestamp (`2026`, `2026-W35-3`,
|
||||||
|
// a comma as the decimal separator), so the cursor is normalized here rather
|
||||||
|
// than reaching the query as the string the client sent. It stays a string
|
||||||
|
// because the equality branch of the cursor reads a `Date` as an empty constraint
|
||||||
|
const before = inputs.before && {
|
||||||
|
listChangedAt: moment.utc(inputs.before.listChangedAt, moment.ISO_8601, true).toISOString(),
|
||||||
|
id: inputs.before.id,
|
||||||
|
};
|
||||||
|
|
||||||
const cards = await Card.qm.getByEndlessListId(list.id, {
|
const cards = await Card.qm.getByEndlessListId(list.id, {
|
||||||
before: inputs.before,
|
before,
|
||||||
search: inputs.search,
|
search: inputs.search,
|
||||||
userIds: filterUserIds,
|
userIds: filterUserIds,
|
||||||
labelIds: filterLabelIds,
|
labelIds: filterLabelIds,
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ const getByEndlessListId = async (listId, { before, search, userIds, labelIds })
|
|||||||
query += ` AND card_label.label_id IN (${inValues.join(', ')})`;
|
query += ` AND card_label.label_id IN (${inValues.join(', ')})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Must match the cursor built from the last returned card, otherwise the
|
||||||
|
// limit cuts an arbitrary slice and pages skip or repeat cards
|
||||||
|
query += ' ORDER BY card.list_changed_at DESC, card.id DESC';
|
||||||
query += ` LIMIT ${LIMIT}`;
|
query += ` LIMIT ${LIMIT}`;
|
||||||
|
|
||||||
let queryResult;
|
let queryResult;
|
||||||
|
|||||||
Reference in New Issue
Block a user