Repository navigation
fix(api): stable ordering, bounded limit and documented pagination on v2 (#52) - #53
Conversation
… v2 (#52) - active and active/kml sorted by `created_at`, which incidents don't have, so pages could overlap or skip items; search and 1000ha-burned had no sort. Now `created` desc (active, kml) or `dateTime` desc (search, 1000ha), with `_id` as tie-break. Photo listings get the same tie-break. - `limit` is clamped to 1..1000 (non-integer → endpoint default) instead of being unbounded / becoming 15 for 0 / reaching Mongo negative. - /v2/incidents/active JSON now returns the `paginator` object; `paginator` gains `perPage`, photo `meta` gains `last_page`. - OpenAPI: `page` on every paginated endpoint, `limit` with default/max per endpoint, ordering described, paginated response for active and search, shared Paginator/PhotoPageMeta models, and the missing POST /v2/incidents/{id}/photos/all. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 987bf65596
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| })->when($subRegion, function ($query, $subRegion){ | ||
| return $query->where('sub_regiao', (string)$subRegion); | ||
| }); | ||
| })->orderBy('dateTime', 'desc')->orderBy('_id', 'desc'); |
There was a problem hiding this comment.
Add compound indexes for the new search ordering
For searches without a date restriction, this adds a blocking sort over the entire matching incident archive before returning even the first 50 records. The indexes defined in database/migrations/2026_05_25_120000_create_mongodb_indexes.php cover dateTime and (isFire, dateTime), but neither includes the new _id sort key; MongoDB therefore cannot obtain this complete ordering from those indexes (MongoDB documentation). Unlike the previous unsorted, limited query, each page now requires fetching and sorting all matches, creating a substantial latency and database-load regression on large archives. Add matching compound indexes for the default fire-only search and the all=1 search alongside this ordering change.
Useful? React with 👍 / 👎.
/v2/incidents/search now sorts by (dateTime desc, _id desc). The existing (isFire, dateTime) and (dateTime) indexes don't include _id, so Mongo fell back to a blocking in-memory sort over every match, e.g. the whole fire archive for a search without dates. Add (isFire, dateTime, _id) for the default fire-only search and (dateTime, _id) for all=1; a test asserts the winning plan has no SORT stage. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Closes #52
Audit
GET /v2/incidents/activelimit(1000),pagecreated_at— field doesn't existGET /v2/incidents/active/kmllimit(300),pagecreated_at— doesn't existGET /v2/incidents/searchlimit(50),pagepaginator(undocumented)GET /v2/incidents/1000ha-burnedlimit(500),pagepaginatorGET /v2/photos/latest,GET /v2/incidents/{id}/photos,POST …/photos/all,GET /v2/moderation/photosper_page(clamped),pagecreated_atmetaWithout a real sort, Mongo returns natural order, so walking pages can repeat or skip incidents.
limitwas unbounded (limit=100000000),0silently became 15 (Eloquent default) and negatives reached Mongo.Changes
Endpoints
createddesc (active, kml),dateTimedesc (search, 1000ha) — both indexed — plus_idtie-break; photos also get the_idtie-break.limitclamped to 1..1000; non-integer → endpoint default. Defaults unchanged./v2/incidents/activeJSON response now includespaginator(additive).paginator.perPageand photometa.last_pageadded (additive).Docs
pageparameter on every paginated endpoint (previously only on/v2/photos/latest).limitdocumented per endpoint with default/max and clamping;searchwas missinglimitaltogether.active/searchnow document the paginated response (v2-fires-paginated-list.yaml);Paginatormodel described; photometa→ sharedPhotoPageMetamodel.POST /v2/incidents/{id}/photos/allwas not documented at all — added.security-definedrule on the new operation (all existing operations already fail it).Kept as is (would break clients)
Two pagination styles coexist: incidents use
limit+paginator{currentPage,totalPages,totalItems}, photos useper_page+meta{total,page,per_page}. Both are now documented; unifying them would be a breaking change for the apps./v2/incidents/active,/active/kml,/searchand/1000ha-burnedis now newest-first (previously effectively insertion order).limitabove 1000 is capped at 1000 → clients that relied on a bigger single page must followpaginator.totalPages. (csv2export on search is not paginated and is unaffected.)Tests
tests/Feature/Controllers/IncidentController/PaginationTest.php(5 tests: page walk without overlap + paginator on active, limit clamping/defaults on active/search/1000ha, search order across pages, 1000ha order, photolast_page). All 5 fail onmain; plus 1 index-plan test. Full suite against a throwaway MongoDB 7: 86 pass.Indexes (Codex review)
Sorting search by
(dateTime, _id)was not covered by the existing(isFire, dateTime)/(dateTime)indexes, so Mongo did a blocking in-memory sort over all matches (the whole archive for a search without dates). New migration2026_10_26_180000_add_search_sort_indexesadds(isFire, dateTime, _id)and(dateTime, _id)ondata; a test asserts the search plans use an index scan with noSORTstage (fails without the migration).Not indexed on purpose:
activesorts only the active incidents (hundreds at most), and1000ha-burnedcosts the same as before (itsicnf.burnArea.totalfilter isn't indexed and the total count already scanned every fire).Deploy
php artisan migrate --force— creates the two indexes ondata(online build; may take a little while on the full archive). Without it the endpoints still work, just slower on large searches.No env changes, no queue restart needed.
🤖 Generated with Claude Code