Skip to content

fix: populate startAt cursor for getListFromDB pagination - #419

Draft
sinnaj-r wants to merge 1 commit into
masterfrom
fix-getlistfromdb-cursor-pagination
Draft

fix: populate startAt cursor for getListFromDB pagination#419
sinnaj-r wants to merge 1 commit into
masterfrom
fix-getlistfromdb-cursor-pagination

Conversation

@sinnaj-r

@sinnaj-r sinnaj-r commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

getListFromDB (the Dart-REST implementation used by all Cloud Run services) breaks Firestore pagination when the caller passes orderBy: null together with a startAfterDocument. The generated runQuery request contains startAt: Cursor(values: [], before: false) — Firestore treats the empty cursor as "past the end" and returns zero rows.

Root cause: getQuery builds cursor values by mapping over the (missing) orderBy. _getListLazyChunked already works around this by appending __name__ to the orderBy; getListFromDB did not.

Fix

Extract a static helper computeEffectiveOrderByForPagination that mirrors the pattern from _getListLazyChunked:

  • If the caller passed no orderBy but there are inequality filters (>, >=, <, <=, !=, not-in), seed orderBy from those filter fields. (Firestore requires inequality fields to appear first in orderBy; this is what the chunked path already does.)
  • Always append YustOrderBy(field: '__name__') as a stable tiebreaker so the cursor has a document reference to anchor on.

getListFromDB now applies this helper only when startAfterDocument != null — behavior for non-paginated callers is unchanged.

Reproduction

The bug surfaced when a Univelop customer tried to page through 3000+ ticket records via /api/v2/workspaces/…/records/tickets?filters=[…]&limit=200. Page 1 returned 200 records and a Next-Page header; following the Next-Page URL returned zero rows. Verified against production Firestore that the empty cursor is the culprit.

Test plan

  • New unit tests in test/yust_database_service_dart_test.dart covering:
    • null orderBy + no filter → [__name__]
    • null orderBy + only equality filter → [__name__]
    • null orderBy + inequality filter → [<field>, __name__]
    • range on same field (>= and <=) → single field + __name__
    • explicit orderBy + no filter → orderBy + __name__
    • explicit orderBy already ending with __name__ → not appended twice
    • explicit orderBy + inequality filter → keep caller order, append __name__
  • dart analyze clean
  • End-to-end verification against production Firestore (paired with the Univelop record_api_service fix — pending)

Release

Draft PR while the paired Univelop change (record_api_service.dart cursor index + classifier) is validated end-to-end. Ready to merge + release as 3.33.3 once the Univelop side is green.

🤖 Generated with Claude Code

When `getListFromDB` is called with `orderBy: null` and a
`startAfterDocument`, `getQuery` builds `Cursor(values: [], before: false)`
because cursor values are mapped from the (empty) orderBy list. Firestore
then treats the cursor as "past the end" and returns zero records, so
callers can never advance beyond the first page.

Extract the effective orderBy computation into a static helper and use it
in `getListFromDB` when `startAfterDocument != null`. Mirrors the pattern
already used by `_getListLazyChunked`:
- if the caller passed no orderBy but there are inequality filters, seed
  orderBy from those filter fields (Firestore requires inequality fields
  to appear first in orderBy anyway).
- always append `__name__` as a stable tiebreaker so the cursor has a
  document reference to anchor on.

Behavior is unchanged when `startAfterDocument == null`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sinnaj-r
sinnaj-r force-pushed the fix-getlistfromdb-cursor-pagination branch from 50772fc to f7493ab Compare July 3, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant