Skip to content

feat: add server-side pagination to API endpoints#1464

Open
fabiovincenzi wants to merge 39 commits intofinos:mainfrom
fabiovincenzi:server-side-pagination
Open

feat: add server-side pagination to API endpoints#1464
fabiovincenzi wants to merge 39 commits intofinos:mainfrom
fabiovincenzi:server-side-pagination

Conversation

@fabiovincenzi
Copy link
Copy Markdown
Contributor

@fabiovincenzi fabiovincenzi commented Mar 20, 2026

Fixes #1297
Implements server side pagination for /push, /repo and /users endpoints.

Note: The list endpoints (/push, /repo, /users) now return { data, total } instead of a plain array to support pagination metadata.

@fabiovincenzi fabiovincenzi requested a review from a team as a code owner March 20, 2026 11:16
@netlify
Copy link
Copy Markdown

netlify bot commented Mar 20, 2026

Deploy Preview for endearing-brigadeiros-63f9d0 canceled.

Name Link
🔨 Latest commit e737d6e
🔍 Latest deploy log https://app.netlify.com/projects/endearing-brigadeiros-63f9d0/deploys/69c69ebc12a5650008c7f81a

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 92.28188% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.73%. Comparing base (4e2eea8) to head (e737d6e).

Files with missing lines Patch % Lines
src/service/routes/push.ts 55.00% 9 Missing ⚠️
src/db/file/users.ts 85.00% 3 Missing ⚠️
src/db/mongo/pushes.ts 93.02% 3 Missing ⚠️
src/db/mongo/repo.ts 83.33% 3 Missing ⚠️
src/db/mongo/users.ts 90.00% 3 Missing ⚠️
src/db/file/helper.ts 92.59% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1464      +/-   ##
==========================================
+ Coverage   89.66%   89.73%   +0.06%     
==========================================
  Files          68       68              
  Lines        4869     5048     +179     
  Branches      888      931      +43     
==========================================
+ Hits         4366     4530     +164     
- Misses        485      500      +15     
  Partials       18       18              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@dcoric dcoric left a comment

Choose a reason for hiding this comment

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

Thanks for the solid work on server-side pagination. I have a few concerns - mostly around security and a couple of behavioral issues that should be addressed before merging.

@kriswest kriswest linked an issue Mar 23, 2026 that may be closed by this pull request
Copy link
Copy Markdown

@roskosm1 roskosm1 left a comment

Choose a reason for hiding this comment

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

docs API breaking changes?

Copy link
Copy Markdown
Contributor

@jescalada jescalada left a comment

Choose a reason for hiding this comment

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

Oddly, the repo list sorting doesn't seem to be working right on my end - whenever I change the sorting order, the dropdown selector just resets (and only the default alphabetical order is shown regardless of which one I choose).

Related to that, when I move to another page the selected filter gets reset, which (might) be resetting the filter to alphabetical ascending order.

I don't get any error output on console, BTW 🤷🏼

});
});

return Promise.all([dataPromise, countPromise]).then(([data, total]) => ({ data, total }));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we should change the data variable to something more descriptive... Right now, we end up calling response.data.data to fetch the repos/users/etc. which can trip up people/agents working with the code.

I'd suggest having the name of the entity { users, total }, etc. although we'd need to pass in the entity name to paginatedFind (unless it's already obtainable in the existing function parameters).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point, the HTTP routes now return entity-specific keys (pushes, repos, users) instead of data. The data variable inside paginatedFind remains as a generic internal name but not exposed directly.

try {
const cookies = JSON.parse(fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8'));
const { data } = await axios.get<Action[]>(`${baseUrl}/api/v1/push/`, {
const response = await axios.get<{ data: Action[]; total: number }>(`${baseUrl}/api/v1/push/`, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can use PaginatedResult type here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the /push endpoint now returns pushes instead of data.

type: 'push',
};

const pushProjection = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps this could be a constant (and potentially placed somewhere else)? Wondering if there are any other constants from the db module that we could wrap into a /db/constants.ts file.

On another note, I'm wondering why we have been doing manual projection at all - if we want to exclude certain fields, we could do post-processing just like we do with users (toPublicUser).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixing this should also help simplify the tests that rely on projection 👍🏼

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure about the reasons for projections here, filtering at the DB level avoids transferring data that is not needed, such as steps which can be quite large. Post-processing like toPublicUser, would work but at the cost of fetching full documents first. Not sure though if this is the reason

Would it be worth tracking in a separate issue/PR to keep this one focused?

const skip = (page - 1) * limit;

const pagination: PaginationOptions = { skip, limit };
if (req.query['search']) pagination.search = req.query['search'] as string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Wondering if we could have stronger typing for req here - so we could do the following:

const { search, sortBy, sortOrder } = req.query;

Extending the Request type can be a huge pain, but it's been done before with the user property for the passport strategies. This might be more appropriate for a separate PR if it requires too many extra changes!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, introduced a PaginationQuery interface and cast req.query once at the top, allowing destructuring without repeated as string casts. I didn't make the global Request type since query params vary per route.

@fabiovincenzi fabiovincenzi self-assigned this Mar 24, 2026
@fabiovincenzi
Copy link
Copy Markdown
Contributor Author

fabiovincenzi commented Mar 25, 2026

Oddly, the repo list sorting doesn't seem to be working right on my end - whenever I change the sorting order, the dropdown selector just resets (and only the default alphabetical order is shown regardless of which one I choose).

Related to that, when I move to another page the selected filter gets reset, which (might) be resetting the filter to alphabetical ascending order.

I don't get any error output on console, BTW 🤷🏼

@jescalada Good catch. I tried to replicate the client side sorting behaviour we had before without verifying that lastModified and dateCreated actually exist on the Repo model and they don't, so the sort was always falling back to the default (name). On the old client-side implementation it appeared to work there because the stable sort preserved DB insertion order, which happened to differ from alphabetical. To fix this properly we'd need to add
these fields to the model and populate them, but maybe this is out of scope for this PR? Maybe removing Date Modified and Date Created from the dropdown and open a separate issue to implement them correctly?
What do you think?

@kriswest
Copy link
Copy Markdown
Contributor

To fix this properly we'd need to add these fields to the model and populate them, but maybe this is out of scope for this PR?

This might be a breaking change unless you include a migration of some sort for existing DBs or at least some form of backwards compatibility.

Maybe removing Date Modified and Date Created from the dropdown and open a separate issue to implement them correctly?

much simpler and non-breaking. If opening a ticket ensure it covers migration/consideration of how to make the change non-breaking (which would usually involve the ability to roll back!).

n.b. dates could possible be extracted from logs in existing records...

@fabiovincenzi
Copy link
Copy Markdown
Contributor Author

In this PR, list endpoints were changed from returning a plain array to returning { <entity>: [...], total: N }, and the data field was renamed to entity-specific keys to avoid the confusing response.data.data pattern. Both are technically breaking changes for any external client.

For the rename (data → pushes/repos/users), we could keep data as an alias. For the array, the only clean backwards-compatible solution would be to keep the existing endpoints returning the plain array and add new paginated endpoints alongside them, but that means maintaining duplicate logic until the old ones are deprecated.

Any thoughts?

@kriswest
Copy link
Copy Markdown
Contributor

I think you've hit on the right answer there - you need to create new endpoints with the new behaviour then switch UI services and the UI itself over to them. Clearly mark the old ones are deprecated and consider where we can provide messaging about them being deprecated.

If we'd got this into the v2 scope, we wouldn't need to do that I guess... WDYT @jescalada

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sort entries in the admin UI and apply proper pagination Improve pagination defaults and layout in admin UI

6 participants