Fix GraphQL: resume a cursor whose row is gone at its position
Fixes #11206
When the row behind an after or before cursor had been deleted or filtered out, the fallback compared the cursor's ISO text with the row's Date and ignored the sort direction. The list then restarted at the first row, or before gave an empty page. The fallback now reads the cursor value on the row's scale and follows the sort direction. Ties on the first key are broken by the id when the id is the next sort key, in either direction. With a longer sort, like channels (isDefault, then createdAt), rows tied on the first key may repeat but are never skipped. A cursor spells a null and an empty text alike, so on a text column the fallback takes the side that repeats rows rather than skipping them. Cursors whose row still exists, the refusals, pageInfo and the cursor format are unchanged.
Summary by cubic
Fixes #11206: pagination now resumes correctly when the row behind an after or before cursor has been deleted or filtered out.
- The fallback reads the cursor value on the row's scale and follows the sort direction; first-key ties are broken by the id when the id is the next sort key, otherwise the walk resumes at the start or end of the tie group so rows repeat but are never skipped.
- On text columns an empty cursor value maps to the side that repeats rows; on non-text columns it reads as a null, whether the kind is declared or inferred from the rows' values (covers sortable-only fields like
contactBuyersandcontactGroups). - Cursors whose row still exists, refusals,
pageInfo, and the cursor format are unchanged. - Adds tests covering
after/beforearound a deleted row across severalcreatedAt/idsort orders, plus null-tie cases for a declared DATE field and a sortable-only numeric field.