feat: add limit/pagination to notifications endpoint (#140) #150
No reviewers
Labels
No Label
ai-changes-requested
ai-failed
ai-merged
ai-pr-opened
ai-reviewed
ai-reviewing
ai-reviewing
ai-working
bug
enhancement
evolution
performance
phase-0
phase-1a
phase-1b
phase-1c
phase-1d
security
tech-debt
todo
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-database#150
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "issue/140-feat-add-limit-pagination-to-notifications-endpoin"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #140
Adds an optional
limitquery parameter toGET /api/v2/notifs:100500(capped server-side)created_after,title,desc, etc.)Files changed
app/routers_v2/notifications.pyOther observations
limitparameter addition.Cannot access attribute "id" for class "type[Notification]"on line 39 is pre-existing and not introduced by this PR.AI Code Review
Files Reviewed
app/routers_v2/notifications.py(modified).gitea/workflows/build.yml(in diff, but appears already merged to main — see note below)Findings
Correctness
limitdefaults to 100, caps at 500 viamin(limit, 500), applied after all existing filters. Matches PR description exactly.limitcauses 500:min(-1, 500)→-1, thenall_notif.limit(-1)generatesLIMIT -1SQL which PostgreSQL rejects with an error. Add a floor:limit = max(0, min(limit, 500)). Alimit=0would return an empty list cleanly (arguably correct; a zero-limit request is a no-op).countfield is now the limited count, not the total: After applying.limit(100), Peewee wraps it asSELECT COUNT(*) FROM (SELECT ... LIMIT 100)— socountin the JSON response equalsmin(actual_matching_rows, limit)rather than the true total. Callers usingcountto determine whether more results exist (for pagination purposes) will get a misleading value once the dataset exceeds the limit. No action needed if this endpoint is only used for monitoring/display, but worth noting if pagination is intended.Security
limitis typedOptional[int]; FastAPI validates it before the handler runs. No injection risk.Style & Conventions
Suggestions
build.ymldiff appears to contain the same CI changes already merged tomainvia commitse12dac3andd4d93cd. This branch looks like it was created before those commits and wasn't rebased. The merge should be clean (no-op for the CI file), but a rebase would keep the PR diff scoped to just the intended change.Verdict: COMMENT
Core implementation is correct and safe. Two minor observations: negative
limitvalues will produce a 500 (easy fix: addmax(0, ...)to the clamp), and thecountfield in JSON responses will reflect the limited count rather than the true total oncelen(results) == limit. Neither blocks merge for typical usage, but the negative-limit case is worth patching to avoid unexpected 500s.Automated review by Claude PR Reviewer
@ -33,0 +31,4 @@in_desc: Optional[str] = None,about: Optional[str] = None,ack: Optional[bool] = None,csv: Optional[bool] = None,Negative
limitvalues (e.g.?limit=-1) survivemin(-1, 500)unchanged and will cause a PostgreSQL error onLIMIT -1. Consider:limit = max(0, min(limit, 500))AI Code Review
Files Reviewed
app/routers_v2/notifications.py(modified)Findings
Correctness
🔴
countfield silently broken by limit — line ~88After applying
.limit(), the JSON response computes count from the limited queryset:In Peewee,
.count()wraps the query as a subquery (SELECT COUNT(1) FROM (SELECT ... LIMIT N) AS _), so it respects the limit. This meanscountnow reflects how many items were returned, not how many total notifications match the filters.This is a breaking change to the existing API contract. Before this PR,
countalways reflected the total matching records — any caller usingcountto determine if there are more results (e.g. for pagination logic) will now silently get wrong data. With the new defaultlimit=100, every unfiltered call now returnscount: 100instead of the actual total.Fix: Capture the total count before applying the limit:
This preserves backward compatibility. Alternatively, return both
totalandreturnedfields — but keepingcountas the pre-limit total is the least-breaking path.Security
limitparameter is an integer clamped server-side viamax(0, min(limit, 500))before reaching the ORM. No injection risk.Style & Conventions
if limit is not None:guard for clamping (line ~33) is technically harmless but slightly misleading — withOptional[int] = 100as the default, FastAPI will reject a null value for anintquery param, solimitwill never beNonein practice. The guard on the.limit()application (line ~68) has the same property. Not a blocker, but the annotation could beint = 100withoutOptionalto make the intent clear.Suggestions
limit=0is valid after clamping, returning an empty result set. This is technically fine but may surprise callers. A minimum of 1 (max(1, min(limit, 500))) or a 422 viaQuery(ge=1, le=500)would give clearer semantics.created_afterfilter direction (Notification.created < created_after_dt) looks inverted for a "created after" semantic — but this is out of scope for this PR.Verdict: REQUEST_CHANGES
The limit/clamping logic is sound and the feature intent is correct, but the
countfield in the JSON response will now silently return the limited count rather than the total matching count. This is a breaking behavioral change to an existing field — callers relying oncountfor pagination awareness will receive wrong data. Fix by capturingtotal_count = all_notif.count()before applying.limit().Automated review by Claude PR Reviewer
@ -62,3 +86,3 @@return Response(content=return_val, media_type='text/csv')return Response(content=return_val, media_type="text/csv")After
all_notif = all_notif.limit(limit)is applied, this.count()call wraps the limited queryset in a subquery — so it returns the count of limited results, not the total matching count. Capturetotal_count = all_notif.count()before applying.limit()and use that here to preserve the existing API contract.AI Code Review
Files Reviewed
app/routers_v2/notifications.py(modified)Findings
Correctness
total_countcapture is correct.total_count = all_notif.count()is captured after all filters are applied but before.limit()is applied. This means thecountfield in the JSON response returns the total number of matching records, not just the size of the returned page — which is the correct behavior for a paginated API.max(0, min(limit, 500))properly enforces the 0–500 range. Alimit=0will produceLIMIT 0in SQL (returns no rows), which is consistent and intentional.Optional[int] = 100is sound. If thelimitparam is omitted by the client, it defaults to 100. If explicitly passed as null (unusual), theif limit is not Noneguard skips the cap and the Peewee query runs unlimited — this is intentional escape-hatch behavior consistent with the rest of the endpoint's filter params.limitis applied toall_notifbefore iteration, so CSV exports also respect the limit. No inconsistency between the two response formats.Notification.select().count()), not the filtered count. This means a filtered query that returns zero results won't raise a 404 — it will return{"count": 0, "notifs": []}. This is a pre-existing behaviour, not introduced by this PR.Security
limitparameter is typed asint, so FastAPI validates and rejects non-integer inputs before the handler runs. The server-side cap at 500 prevents resource exhaustion from large unbounded queries.Style & Conventions
patch_notif,get_notifs) now follow standard one-arg-per-line style, which is an improvement in readability.Suggestions
limitwas added — nooffsetorpageparameter for true pagination. If the intent from issue #140 is only a result cap rather than cursor/offset pagination, this is complete as-is. If full pagination was intended, anoffset: Optional[int] = 0parameter would complete the feature. This doesn't block merging — just flagging for follow-up if needed.Verdict: APPROVED
Clean, focused implementation. The
total_count/limitsplit is handled correctly, the server-side cap is appropriate, and the ruff reformats are noise-free. No functional regressions detected across the other endpoints in the file. Ready to merge.Automated review by Claude PR Reviewer
Approving after automated review — APPROVED.