Skip to content

Add pagination and optional query params to config list endpoint#725

Open
vprashrex wants to merge 8 commits intomainfrom
feat/adding-query-params-to-config
Open

Add pagination and optional query params to config list endpoint#725
vprashrex wants to merge 8 commits intomainfrom
feat/adding-query-params-to-config

Conversation

@vprashrex
Copy link
Copy Markdown
Collaborator

@vprashrex vprashrex commented Mar 30, 2026

Issue: #732

Summary

  • Previously, the config list endpoint had no search capability and didn't indicate whether more records exist beyond the current page. This made it difficult for clients to:
  • Filter configs by name without fetching everything and filtering client-side
  • Implement proper pagination UI (e.g., "Load more" buttons) since there was no signal for additional records

This PR adds an optional query param for name-based search and a has_more flag in the response metadata, enabling efficient server-side filtering and accurate pagination.

Checklist

Before submitting a pull request, please ensure that you mark these task.

  • Ran fastapi run --reload app/main.py or docker compose up in the repository root and test.
  • If you've fixed a bug or added code that is tested and has test cases.

Notes

Please add here if any other information is required for the reviewer.

Summary by CodeRabbit

  • New Features

    • Add search-by-name for configurations and explicit pagination metadata (has_more) on config and document list responses.
  • Documentation

    • API docs updated to document query, skip, and limit parameters and the pagination metadata.
  • Tests

    • Tests updated/added to cover search filtering and the new pagination metadata behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Added query-based filtering and "has_more" pagination metadata to config and document list flows; CRUD read methods now return (items, has_more); docs updated to document query, skip, and limit parameters.

Changes

Cohort / File(s) Summary
Config API Documentation
backend/app/api/docs/config/list.md
Documented three request parameters: query, skip (default 0), and limit (default/max 100).
Config API & CRUD
backend/app/api/routes/config/config.py, backend/app/crud/config/config.py
Endpoint list_configs accepts query and returns metadata.has_more; ConfigCrud.read_all() gains query param, adds name-prefix filtering, fetches limit+1 for has-more detection, and returns (configs, has_more).
Document API & CRUD
backend/app/api/routes/documents.py, backend/app/crud/document/document.py
list_docs now unpacks (documents, has_more) and returns pagination metadata; DocumentCrud.read_many() orders by inserted_at.desc(), fetches limit+1, and returns (documents, has_more).
Tests (configs & documents)
backend/app/tests/api/routes/configs/test_config.py, backend/app/tests/crud/config/test_config.py, backend/app/tests/crud/documents/..., backend/app/tests/crud/documents/documents/test_crud_document_update.py
Updated tests to expect tuple return (items, has_more), added tests for query filtering on configs, and adjusted assertions to use metadata.has_more where applicable.

Sequence Diagram

sequenceDiagram
    participant Client
    participant APIRoute
    participant CRUD
    participant DB as Database

    Client->>APIRoute: GET /configs?query=prefix&skip=0&limit=100
    APIRoute->>CRUD: read_all(query='prefix', skip=0, limit=100)
    CRUD->>DB: SELECT ... WHERE name ILIKE 'prefix%' AND project_id=... AND deleted_at IS NULL LIMIT 101 OFFSET 0
    DB-->>CRUD: rows (up to 101)
    CRUD->>CRUD: truncate to 100 if > limit\nset has_more = (len > limit)
    CRUD-->>APIRoute: (configs[:limit], has_more)
    APIRoute->>Client: 200 { data: [...], metadata: { has_more: true/false } }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I sniff the configs for a matching name,

fetching one more to know if more came.
Skip, limit, query — I hop and I store,
returning my bounty with has_more galore.
Tiny paws, big search, hooray! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main change: adding pagination and optional query parameters to the config list endpoint.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adding-query-params-to-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/crud/document/document.py (1)

36-77: ⚠️ Potential issue | 🔴 Critical

Critical: Return type change breaks all existing tests that expect a list.

The return type change from list[Document] to tuple[list[Document], bool] causes failures in test code:

  • backend/app/tests/crud/documents/documents/test_crud_document_read_many.py (lines 27–114): Multiple assertions fail—len(docs) returns 2 instead of the actual document count, and all(x.is_deleted is False for x in crud.read_many()) attempts to iterate over (list, bool), causing a TypeError.
  • backend/app/tests/crud/documents/documents/test_crud_document_update.py (lines 21–25): len(before) and len(after) will both return 2.

All test assertions must unpack the tuple: docs, has_more = crud.read_many(...) instead of treating the result as a list. The API route at backend/app/api/routes/documents.py:84 is already correctly unpacking the tuple.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/crud/document/document.py` around lines 36 - 77, The read_many
function now returns (list[Document], bool) but tests (and some callers) still
treat its return as a plain list; update every callsite and test to unpack the
tuple (e.g., docs, has_more = crud.read_many(...)) and then use docs for
length/iteration and has_more for pagination assertions; search for usages of
read_many in tests like the ones in test_crud_document_read_many and
test_crud_document_update and replace direct iterations/len(calls) with the
unpacked docs variable accordingly.
🧹 Nitpick comments (3)
backend/app/api/routes/documents.py (1)

97-97: Prefer dict literal over dict() call.

Static analysis (Ruff C408) flags this as unnecessary. Use a dict literal for consistency and minor performance improvement.

Suggested fix
-    return APIResponse.success_response(results, metadata=dict(has_more=has_more))
+    return APIResponse.success_response(results, metadata={"has_more": has_more})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/routes/documents.py` at line 97, Replace the dict() call used
for the metadata argument with a dict literal to satisfy Ruff C408: in the
return that calls APIResponse.success_response(results, metadata=...), change
the metadata from dict(has_more=has_more) to a literal mapping with the key
"has_more" and the has_more variable as its value; ensure you update the
expression where results and has_more are passed to
APIResponse.success_response.
backend/app/api/routes/config/config.py (1)

66-69: Use a literal for metadata instead of dict(...).

This avoids Ruff C408 and is more idiomatic.

♻️ Suggested cleanup
     return APIResponse.success_response(
         data=configs,
-        metadata=dict(has_more=has_more)
+        metadata={"has_more": has_more}
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/routes/config/config.py` around lines 66 - 69, Replace the
use of dict(...) when calling APIResponse.success_response with a literal dict:
change metadata=dict(has_more=has_more) to use a mapping literal (e.g.
metadata={"has_more": has_more}) in the return that invokes
APIResponse.success_response so the call (return
APIResponse.success_response(data=configs, metadata=...)) uses a literal and
avoids Ruff C408; update the metadata argument where it appears in this
function.
backend/app/crud/config/config.py (1)

106-108: Redundant None guard on limit can be removed.

Given limit: int in this method signature, limit is not None is unnecessary here.

🧹 Suggested simplification
-        if limit is not None and len(configs) > limit:
+        if len(configs) > limit:
             has_more = True
             configs = configs[:limit]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/crud/config/config.py` around lines 106 - 108, The conditional
check redundantly tests "limit is not None" even though the function signature
declares limit: int; update the block in the function that manipulates configs
(the code using variables limit, configs, has_more) to simply check "if
len(configs) > limit:" then set has_more = True and trim configs =
configs[:limit]; remove the unnecessary None guard so the logic uses the integer
limit directly while keeping has_more and slicing behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/api/docs/config/list.md`:
- Line 1: Update the docs to state that the query performs a prefix match
against Config.name (i.e., it uses ilike(f"{query}%")) rather than a general
substring search; explicitly mention "prefix match on Config.name" and give an
example like searching "env" will match "env-prod" but not "my-env" to prevent
client confusion.

In `@backend/app/api/routes/config/config.py`:
- Around line 53-59: The function list_configs is missing an explicit return
type; update its signature to include a return annotation that matches the
actual returned value (for example a list of your config schema or a FastAPI
response model), e.g. annotate list_configs with the correct return type after
the parameter list (keeping current_user: AuthContextDep and session: SessionDep
intact); ensure the chosen type (e.g., list[ConfigSchema], ConfigListResponse,
or Response) matches the implementation and imported types so type checking
passes.

---

Outside diff comments:
In `@backend/app/crud/document/document.py`:
- Around line 36-77: The read_many function now returns (list[Document], bool)
but tests (and some callers) still treat its return as a plain list; update
every callsite and test to unpack the tuple (e.g., docs, has_more =
crud.read_many(...)) and then use docs for length/iteration and has_more for
pagination assertions; search for usages of read_many in tests like the ones in
test_crud_document_read_many and test_crud_document_update and replace direct
iterations/len(calls) with the unpacked docs variable accordingly.

---

Nitpick comments:
In `@backend/app/api/routes/config/config.py`:
- Around line 66-69: Replace the use of dict(...) when calling
APIResponse.success_response with a literal dict: change
metadata=dict(has_more=has_more) to use a mapping literal (e.g.
metadata={"has_more": has_more}) in the return that invokes
APIResponse.success_response so the call (return
APIResponse.success_response(data=configs, metadata=...)) uses a literal and
avoids Ruff C408; update the metadata argument where it appears in this
function.

In `@backend/app/api/routes/documents.py`:
- Line 97: Replace the dict() call used for the metadata argument with a dict
literal to satisfy Ruff C408: in the return that calls
APIResponse.success_response(results, metadata=...), change the metadata from
dict(has_more=has_more) to a literal mapping with the key "has_more" and the
has_more variable as its value; ensure you update the expression where results
and has_more are passed to APIResponse.success_response.

In `@backend/app/crud/config/config.py`:
- Around line 106-108: The conditional check redundantly tests "limit is not
None" even though the function signature declares limit: int; update the block
in the function that manipulates configs (the code using variables limit,
configs, has_more) to simply check "if len(configs) > limit:" then set has_more
= True and trim configs = configs[:limit]; remove the unnecessary None guard so
the logic uses the integer limit directly while keeping has_more and slicing
behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c47e6852-6170-4dd0-833a-d4e8974c31e2

📥 Commits

Reviewing files that changed from the base of the PR and between e577369 and 0c7175b.

📒 Files selected for processing (5)
  • backend/app/api/docs/config/list.md
  • backend/app/api/routes/config/config.py
  • backend/app/api/routes/documents.py
  • backend/app/crud/config/config.py
  • backend/app/crud/document/document.py

@@ -1,3 +1,7 @@
query: Search string used for partial matching across configurations
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify query matching semantics in the docs.

Current wording suggests broad partial matching, but the implementation applies a prefix match on Config.name (ilike(f"{query}%")). Please make this explicit to avoid client-side confusion.

✏️ Suggested doc tweak
-query: Search string used for partial matching across configurations
+query: Optional prefix search string for configuration names (case-insensitive)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
query: Search string used for partial matching across configurations
query: Optional prefix search string for configuration names (case-insensitive)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/docs/config/list.md` at line 1, Update the docs to state that
the query performs a prefix match against Config.name (i.e., it uses
ilike(f"{query}%")) rather than a general substring search; explicitly mention
"prefix match on Config.name" and give an example like searching "env" will
match "env-prod" but not "my-env" to prevent client confusion.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
backend/app/tests/crud/documents/documents/test_crud_document_read_many.py (1)

78-96: Consider adding tests for has_more pagination behavior.

Since this PR adds the has_more flag and this file is the primary test suite for read_many(), it would be valuable to add tests verifying the has_more logic:

  1. has_more=True when more records exist beyond the limit
  2. has_more=False when all records fit within the limit
  3. has_more=False when limit=0

Example test:

def test_has_more_true_when_records_exceed_limit(
    self,
    db: Session,
    store: DocumentStore,
) -> None:
    crud = DocumentCrud(db, store.project.id)
    limit = self._ndocs // 2  # Request fewer than total
    docs, has_more = crud.read_many(limit=limit)

    assert len(docs) == limit
    assert has_more is True

def test_has_more_false_when_all_records_returned(
    self,
    db: Session,
    store: DocumentStore,
) -> None:
    crud = DocumentCrud(db, store.project.id)
    docs, has_more = crud.read_many(limit=self._ndocs + 10)

    assert len(docs) == self._ndocs
    assert has_more is False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/tests/crud/documents/documents/test_crud_document_read_many.py`
around lines 78 - 96, Add tests in test_crud_document_read_many.py that assert
the new has_more pagination flag returned by DocumentCrud.read_many: create a
test (e.g., test_has_more_true_when_records_exceed_limit) that calls
DocumentCrud(db, store.project.id).read_many(limit=<fewer than total, e.g.,
self._ndocs // 2>) and asserts len(docs) == limit and has_more is True; create a
test (e.g., test_has_more_false_when_all_records_returned) that calls read_many
with limit larger than total (e.g., self._ndocs + 10) and asserts len(docs) ==
self._ndocs and has_more is False; and add a test (e.g.,
test_has_more_false_when_limit_zero) that calls read_many(limit=0) and asserts
not docs and has_more is False. Ensure you use DocumentCrud and the existing db
and store fixtures so tests mirror the style of existing tests.
backend/app/api/routes/config/config.py (1)

66-66: Prefer dict literal for metadata construction.

At line 66, dict(has_more=has_more) should be replaced with a dictionary literal to comply with Ruff C408.

Suggested patch
-    return APIResponse.success_response(data=configs, metadata=dict(has_more=has_more))
+    return APIResponse.success_response(data=configs, metadata={"has_more": has_more})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/api/routes/config/config.py` at line 66, The return uses
dict(has_more=has_more) which triggers Ruff C408; replace that call with a
dictionary literal so APIResponse.success_response(data=configs,
metadata={'has_more': has_more}) is used instead, locating the change in the
return statement that calls APIResponse.success_response with data=configs and
metadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/tests/api/routes/configs/test_config.py`:
- Around line 477-481: The test currently checks prefix filtering with a
substring assertion; update the assertion that inspects returned_names (the list
comprehension variable `returned_names = [c["name"] for c in data["data"]]`) so
it uses startswith semantics instead of substring containment—replace the
`all("search" in name for name in returned_names)` check with an assertion that
every name starts with "search" (e.g., use `all(name.startswith("search") for
name in returned_names)`) to match the prefix filtering implemented in the CRUD
logic (`ilike(f"{query}%")`).

---

Nitpick comments:
In `@backend/app/api/routes/config/config.py`:
- Line 66: The return uses dict(has_more=has_more) which triggers Ruff C408;
replace that call with a dictionary literal so
APIResponse.success_response(data=configs, metadata={'has_more': has_more}) is
used instead, locating the change in the return statement that calls
APIResponse.success_response with data=configs and metadata.

In `@backend/app/tests/crud/documents/documents/test_crud_document_read_many.py`:
- Around line 78-96: Add tests in test_crud_document_read_many.py that assert
the new has_more pagination flag returned by DocumentCrud.read_many: create a
test (e.g., test_has_more_true_when_records_exceed_limit) that calls
DocumentCrud(db, store.project.id).read_many(limit=<fewer than total, e.g.,
self._ndocs // 2>) and asserts len(docs) == limit and has_more is True; create a
test (e.g., test_has_more_false_when_all_records_returned) that calls read_many
with limit larger than total (e.g., self._ndocs + 10) and asserts len(docs) ==
self._ndocs and has_more is False; and add a test (e.g.,
test_has_more_false_when_limit_zero) that calls read_many(limit=0) and asserts
not docs and has_more is False. Ensure you use DocumentCrud and the existing db
and store fixtures so tests mirror the style of existing tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d26a7699-fc3f-4cca-8679-10b3e575aec3

📥 Commits

Reviewing files that changed from the base of the PR and between 0c7175b and 7f98414.

📒 Files selected for processing (6)
  • backend/app/api/routes/config/config.py
  • backend/app/crud/config/config.py
  • backend/app/tests/api/routes/configs/test_config.py
  • backend/app/tests/crud/config/test_config.py
  • backend/app/tests/crud/documents/documents/test_crud_document_read_many.py
  • backend/app/tests/crud/documents/documents/test_crud_document_update.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/crud/config/config.py

Comment on lines +477 to +481
returned_names = [c["name"] for c in data["data"]]
assert "search-alpha" in returned_names
assert "search-beta" in returned_names
assert all("search" in name for name in returned_names)
assert "other-config" not in returned_names
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Tighten filter assertion to match prefix semantics.

At Line [480], all("search" in name ...) validates substring behavior, but backend/app/crud/config/config.py applies prefix filtering (ilike(f"{query}%")). Assert startswith to prevent false positives.

Suggested patch
-    assert all("search" in name for name in returned_names)
+    assert all(name.lower().startswith("search") for name in returned_names)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
returned_names = [c["name"] for c in data["data"]]
assert "search-alpha" in returned_names
assert "search-beta" in returned_names
assert all("search" in name for name in returned_names)
assert "other-config" not in returned_names
returned_names = [c["name"] for c in data["data"]]
assert "search-alpha" in returned_names
assert "search-beta" in returned_names
assert all(name.lower().startswith("search") for name in returned_names)
assert "other-config" not in returned_names
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/tests/api/routes/configs/test_config.py` around lines 477 - 481,
The test currently checks prefix filtering with a substring assertion; update
the assertion that inspects returned_names (the list comprehension variable
`returned_names = [c["name"] for c in data["data"]]`) so it uses startswith
semantics instead of substring containment—replace the `all("search" in name for
name in returned_names)` check with an assertion that every name starts with
"search" (e.g., use `all(name.startswith("search") for name in returned_names)`)
to match the prefix filtering implemented in the CRUD logic
(`ilike(f"{query}%")`).

@Ayush8923 Ayush8923 changed the title feat: add pagination and optional query params to config list endpoint Add pagination and optional query params to config list endpoint Apr 1, 2026
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.

2 participants