Skip to content

feat: add pagination to item endpoint by using fastapi-pagination #89

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

carvalhocaio
Copy link

@carvalhocaio carvalhocaio commented Jul 9, 2025

Endpoint Pagination #73

Description

using fastapi-pagination I added pagination to the item listing endpoint. I made the adjustment to the endpoint for dynamic pagination.

Motivation and Context

Because there was no list, the item rendering had a very large scroll, which wasn't visually appealing, and it also returned all the items at once. With fast pagination, we return a specific number of items and control the number of items displayed on the frontend, which makes the experience much better.

Screenshots (if appropriate):

image

Steps to reproduce (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires documentation updates.
  • I have updated the documentation accordingly.
  • My change requires dependencies updates.
  • I have updated the dependencies accordingly.

Summary by Sourcery

Implement end-to-end pagination for the items API: enable paginated queries on the backend, update OpenAPI types and SDK, and update the frontend dashboard to fetch, display, and navigate through paginated item lists.

New Features:

  • Add pagination parameters to backend GET /items endpoint using fastapi-pagination
  • Introduce page size selector and pagination controls in frontend dashboard
  • Extend OpenAPI client SDK to support pagination query parameters

Enhancements:

  • Integrate fastapi-pagination library in backend and update project version to 0.0.4
  • Generate new pagination-related TypeScript types in the OpenAPI client
  • Add UI components for pagination controls and page size selection

Build:

  • Update backend and frontend lockfiles and bump fastapi-pagination and @radix-ui/react-select dependencies

CI:

  • Bump pre-commit hook revisions for pre-commit-hooks and ruff-pre-commit

Documentation:

  • Add CHANGELOG entry for items pagination in version 0.0.4

Tests:

  • Enhance item endpoint tests to assert pagination response structure and commit data before querying

Copy link

sourcery-ai bot commented Jul 9, 2025

Reviewer's Guide

Integrates fastapi-pagination into the backend item endpoint and extends the Next.js frontend with dynamic page & size parameters, new pagination UI components, updated API client types/SDK, enhanced tests, and version/dependency bumps to support paginated item listings.

File-Level Changes

Change Details Files
Backend pagination integration using fastapi-pagination
  • Added fastapi-pagination dependency and initialized it in main.py
  • Created transform_items helper for ItemRead validation
  • Replaced raw select with apaginate call using Params(page, size)
  • Exposed page & size Query parameters in read_item endpoint
fastapi_backend/app/main.py
fastapi_backend/app/routes/items.py
fastapi_backend/pyproject.toml
fastapi_backend/requirements.txt
Frontend DashboardPage updated for paginated fetching and controls
  • Extended fetchItems to accept page & size and include them in readItem query
  • Modified DashboardPage to read searchParams, compute pagination, and fetch accordingly
  • Inserted PageSizeSelector and PagePagination components into the page layout
nextjs-frontend/components/actions/items-action.ts
nextjs-frontend/app/dashboard/page.tsx
New pagination UI components and select primitive
  • Added PagePagination component with controls for first/prev/next/last pages
  • Added PageSizeSelector component to select items per page
  • Introduced a reusable Select UI wrapper around Radix primitives
nextjs-frontend/components/page-pagination.tsx
nextjs-frontend/components/page-size-selector.tsx
nextjs-frontend/components/ui/select.tsx
API client types and SDK updated for paginated responses
  • Defined Page_ItemRead_ model and aliased ReadItemResponse to it
  • Extended ReadItemData with page/size query parameters
  • Adjusted readItem SDK signature to use the new query type
nextjs-frontend/app/openapi-client/types.gen.ts
nextjs-frontend/app/openapi-client/sdk.gen.ts
Test enhancements for pagination response structure
  • Committed DB session before fetching in tests
  • Asserted pagination envelope keys (items, total, page, size)
  • Filtered and validated test items within paginated results
fastapi_backend/tests/routes/test_items.py
Version, changelog, and tooling updates
  • Bumped backend version and updated CHANGELOG.md for 0.0.4 with pagination entry
  • Added fastapi-pagination to lock and requirements files
  • Upgraded pre-commit-hooks and ruff versions
fastapi_backend/pyproject.toml
CHANGELOG.md
.pre-commit-config.yaml
fastapi_backend/requirements.txt

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @carvalhocaio - I've reviewed your changes - here's some feedback:

  • In DashboardPage, searchParams is not a promise in Next.js Server Components—change its type to a plain object and remove the await searchParams to simplify the code.
  • When rendering table rows, avoid using the array index as the key—use a stable unique identifier like item.id to prevent UI rerender issues.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In DashboardPage, `searchParams` is not a promise in Next.js Server Components—change its type to a plain object and remove the `await searchParams` to simplify the code.
- When rendering table rows, avoid using the array index as the key—use a stable unique identifier like `item.id` to prevent UI rerender issues.

## Individual Comments

### Comment 1
<location> `nextjs-frontend/app/dashboard/page.tsx:23` </location>
<code_context>

-export default async function DashboardPage() {
-  const items = (await fetchItems()) as ReadItemResponse;
+interface DashboardPageProps {
+  searchParams: Promise<{
+    page?: string;
</code_context>

<issue_to_address>
The type of searchParams as Promise<{ page?: string; size?: string; }> is unconventional for Next.js route handlers.

In the Next.js app directory, searchParams should be a plain object, not a Promise. Using a Promise here may cause confusion or require unnecessary awaiting. Use searchParams: { page?: string; size?: string; } unless a Promise is specifically needed.

Suggested implementation:

```typescript
interface DashboardPageProps {
  searchParams: {
    page?: string;
    size?: string;
  };
}

```

```typescript
export default async function DashboardPage({
  searchParams,
}: DashboardPageProps) {
  const page = Number(searchParams.page) || 1;
  const size = Number(searchParams.size) || 10;

```
</issue_to_address>

### Comment 2
<location> `nextjs-frontend/components/page-pagination.tsx:34` </location>
<code_context>
+  return (
+    <div className="flex items-center justify-between my-4">
+      <div className="text-sm text-gray-600">
+        Showing {(currentPage - 1) * pageSize + 1} to{" "}
+        {Math.min(currentPage * pageSize, totalItems)} of {totalItems} results
+      </div>
+
</code_context>

<issue_to_address>
The calculation for the 'Showing X to Y' range does not handle the case when there are zero results.

When totalItems is 0, the range displays 'Showing 1 to 0 of 0 results', which is unclear. Suggest displaying 'Showing 0 of 0 results' or hiding the range when there are no items.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
  return (
    <div className="flex items-center justify-between my-4">
      <div className="text-sm text-gray-600">
        Showing {(currentPage - 1) * pageSize + 1} to{" "}
        {Math.min(currentPage * pageSize, totalItems)} of {totalItems} results
      </div>
=======
  return (
    <div className="flex items-center justify-between my-4">
      <div className="text-sm text-gray-600">
        {totalItems === 0 ? (
          <>Showing 0 of 0 results</>
        ) : (
          <>
            Showing {(currentPage - 1) * pageSize + 1} to{" "}
            {Math.min(currentPage * pageSize, totalItems)} of {totalItems} results
          </>
        )}
      </div>
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `nextjs-frontend/components/page-pagination.tsx:60` </location>
<code_context>
+        </Link>
+
+        {/* Page Info */}
+        <span className="text-sm font-medium">
+          Page {currentPage} of {totalPages}
+        </span>
+
</code_context>

<issue_to_address>
Displaying 'Page X of Y' when totalPages is 0 may be misleading.

When totalPages is 0, the UI displays 'Page 1 of 0'. Consider hiding this info or ensuring totalPages is at least 1 to avoid confusion.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
        {/* Page Info */}
+        <span className="text-sm font-medium">
+          Page {currentPage} of {totalPages}
+        </span>
=======
        {/* Page Info */}
        {totalPages > 0 && (
          <span className="text-sm font-medium">
            Page {currentPage} of {totalPages}
          </span>
        )}
>>>>>>> REPLACE

</suggested_fix>

### Comment 4
<location> `fastapi_backend/tests/routes/test_items.py:68` </location>
<code_context>
+
+        items = response_data["items"]
+
+        # Filter items created in this test (to avoid interference from other tests)
+        test_items = [
+            item for item in items if item["name"] in ["First Item", "Second Item"]
+        ]

-        assert len(items) == 2
-        assert any(item["name"] == "First Item" for item in items)
-        assert any(item["name"] == "Second Item" for item in items)
+        assert len(test_items) == 2
+        assert any(item["name"] == "First Item" for item in test_items)
+        assert any(item["name"] == "Second Item" for item in test_items)

     @pytest.mark.asyncio(loop_scope="function")
</code_context>

<issue_to_address>
Missing tests for pagination edge cases (e.g., page boundaries, empty pages, custom page sizes).

Please add tests for: exceeding page numbers (should return empty items), custom page sizes (verify item count and metadata), last page correctness, and invalid page/size values (ensure proper error handling). This will improve coverage of pagination edge cases.
</issue_to_address>

### Comment 5
<location> `nextjs-frontend/app/openapi-client/types.gen.ts:60` </location>
<code_context>
   client_secret?: string | null;
 };

+export type Page_ItemRead_ = {
+  items: Array<ItemRead>;
+  total?: number | null;
</code_context>

<issue_to_address>
Consider introducing a generic paginated response type and reusing it to simplify and unify paginated data structures.

```suggestion
// Introduce a reusable generic for all paginated responses
export type Paginated<T> = {
  items: T[];
  total?: number | null;
  page?: number | null;
  size?: number | null;
  pages?: number | null;
};

// Collapse Page_ItemRead_ into a single well-named alias
export type ReadItemResponse = Paginated<ItemRead>;

// Rename ReadItemData to reflect its intent, and inline query params
export type ReadItemParams = {
  query?: {
    /** Page number */
    page?: number;
    /** Page size */
    size?: number;
  };
};

// Remove these now-unnecessary types:
// - export type Page_ItemRead_ = { … }
// - export type ReadItemData = { … }
```
</issue_to_address>

### Comment 6
<location> `nextjs-frontend/components/ui/select.tsx:3` </location>
<code_context>
+"use client";
+
+import * as React from "react";
+import * as SelectPrimitive from "@radix-ui/react-select";
+import { cn } from "@/lib/utils";
</code_context>

<issue_to_address>
Consider refactoring the component by using a factory function for forwardRef wrappers, merging scroll buttons, and only exporting used primitives to reduce boilerplate.

```markdown
I think we can keep full Radix-select functionality but cut down on boilerplate by:

1. Generating the identical `forwardRef` wrappers with a small factory  
2. Merging ScrollUp/ScrollDown into one component  
3. Only exporting the pieces you actually use in your page-size selector

## 1) Component factory

```ts
import * as SelectPrimitive from "@radix-ui/react-select";
import { cn } from "@/lib/utils";
import React from "react";

type Prim = typeof SelectPrimitive;
type CompName = keyof Prim;

function createSelect<K extends CompName>(
  key: K,
  baseClass: string,
  defaultProps: Partial<React.ComponentPropsWithoutRef<Prim[K]>> = {}
) {
  const Component = React.forwardRef<any, React.ComponentPropsWithoutRef<Prim[K]>>(
    ({ className, ...props }, ref) => {
      const Pr = (SelectPrimitive as any)[key];
      return (
        <Pr
          ref={ref}
          className={cn(baseClass, className)}
          {...defaultProps}
          {...props}
        />
      );
    }
  );
  Component.displayName = `Select${key}`;
  return Component as React.FC<React.ComponentPropsWithoutRef<Prim[K]>>;
}
```

## 2) ScrollButton

```ts
import { ChevronUpIcon, ChevronDownIcon } from "@radix-ui/react-icons";

const scrollBase = "flex cursor-default items-center justify-center py-1";
export const SelectScrollButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> & {
    direction?: "up" | "down";
  }
>(({ direction = "up", className, ...props }, ref) => {
  const Icon = direction === "up" ? ChevronUpIcon : ChevronDownIcon;
  const Prim = direction === "up"
    ? SelectPrimitive.ScrollUpButton
    : SelectPrimitive.ScrollDownButton;

  return (
    <Prim ref={ref} className={cn(scrollBase, className)} {...props}>
      <Icon className="h-4 w-4" />
    </Prim>
  );
});
SelectScrollButton.displayName = "SelectScrollButton";
```

## 3) Your exports

```ts
export const SelectTrigger = createSelect(
  "Trigger",
  "flex h-9 w-full items-center justify-between …"
);
export const SelectContent = createSelect(
  "Content",
  "relative z-50 max-h-… shadow-md …",
  { asChild: false }    // whatever defaults you need
);
// only if you use these:
export const SelectValue = SelectPrimitive.Value;
export const SelectItem  = createSelect("Item", "relative flex w-full …");
export const SelectSeparator = createSelect("Separator", "-mx-1 my-1 h-px bg-muted");
```

This shrinks ~160 lines of near-duplicate `forwardRef` code down to ~30 lines, preserves all functionality, and makes it trivial to add/remove primitives later.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@andersonresende andersonresende self-requested a review July 9, 2025 19:54
@andersonresende
Copy link
Collaborator

Thanks @carvalhocaio! I'll check it soon! ;)

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