Skip to content

Commit f8e7f08

Browse files
Merge pull request #104 from bartoszclapinski/sprint2/phase2.C.5-api-pagination-#95
Sprint2/phase2.c.5 api pagination #95
2 parents c45f8cd + b37335b commit f8e7f08

File tree

2 files changed

+157
-21
lines changed

2 files changed

+157
-21
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace DevMetricsPro.Application.DTOs.Common;
2+
3+
/// <summary>
4+
/// Generic wrapper for paginated API responses
5+
/// </summary>
6+
/// <typeparam name="T">The type of items in the result set</typeparam>
7+
public class PaginatedResult<T>
8+
{
9+
/// <summary>
10+
/// The items for the current page
11+
/// </summary>
12+
public IEnumerable<T> Items { get; set; } = new List<T>();
13+
14+
/// <summary>
15+
/// Current page number (1-based)
16+
/// </summary>
17+
public int Page { get; set; }
18+
19+
/// <summary>
20+
/// Number of items per page
21+
/// </summary>
22+
public int PageSize { get; set; }
23+
24+
/// <summary>
25+
/// Total number of items across all pages
26+
/// </summary>
27+
public int TotalCount { get; set; }
28+
29+
/// <summary>
30+
/// Total number of pages
31+
/// </summary>
32+
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)TotalCount / PageSize) : 0;
33+
34+
/// <summary>
35+
/// Whether there is a previous page
36+
/// </summary>
37+
public bool HasPreviousPage => Page > 1;
38+
39+
/// <summary>
40+
/// Whether there is a next page
41+
/// </summary>
42+
public bool HasNextPage => Page < TotalPages;
43+
}
44+

src/DevMetricsPro.Web/Controllers/GitHubController.cs

Lines changed: 113 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DevMetricsPro.Application.Caching;
22
using DevMetricsPro.Application.DTOs.GitHub;
3+
using DevMetricsPro.Application.DTOs.Common;
34
using DevMetricsPro.Application.Interfaces;
45
using DevMetricsPro.Core.Entities;
56
using DevMetricsPro.Core.Enums;
@@ -452,7 +453,77 @@ public async Task<IActionResult> SyncCommits(long githubRepositoryId, Cancellati
452453
}
453454

454455
/// <summary>
455-
/// Get recent commits across all repositories for the authenticated user
456+
/// Get commits with pagination support
457+
/// </summary>
458+
[HttpGet("commits")]
459+
[Authorize(AuthenticationSchemes = "Bearer")]
460+
[ProducesResponseType(typeof(PaginatedResult<object>), StatusCodes.Status200OK)]
461+
public async Task<IActionResult> GetCommits(
462+
[FromQuery] Guid? repositoryId = null,
463+
[FromQuery] int page = 1,
464+
[FromQuery] int pageSize = 25,
465+
CancellationToken cancellationToken = default)
466+
{
467+
if (!TryGetUserId(out var userId))
468+
{
469+
return Unauthorized(new {error = "User not authenticated"});
470+
}
471+
472+
// Validate and clamp pagination parameters
473+
page = Math.Max(1, page);
474+
pageSize = Math.Clamp(pageSize, 10, 100);
475+
476+
var commitRepo = _unitOfWork.Repository<Commit>();
477+
var query = commitRepo.Query()
478+
.Include(c => c.Developer)
479+
.Include(c => c.Repository)
480+
.AsQueryable();
481+
482+
// Filter by repository if specified
483+
if (repositoryId.HasValue)
484+
{
485+
query = query.Where(c => c.RepositoryId == repositoryId.Value);
486+
}
487+
488+
// Get total count before pagination
489+
var totalCount = await query.CountAsync(cancellationToken);
490+
491+
// Apply pagination
492+
var commits = await query
493+
.OrderByDescending(c => c.CommittedAt)
494+
.Skip((page - 1) * pageSize)
495+
.Take(pageSize)
496+
.Select(c => new
497+
{
498+
sha = c.Sha,
499+
message = c.Message,
500+
authorName = c.Developer != null ? c.Developer.DisplayName ?? "Unknown Author" : "Unknown Author",
501+
committedAt = c.CommittedAt,
502+
repositoryName = c.Repository != null ? c.Repository.Name ?? "Unknown Repository" : "Unknown Repository",
503+
repositoryId = c.RepositoryId,
504+
linesAdded = c.LinesAdded,
505+
linesRemoved = c.LinesRemoved,
506+
filesChanged = c.FilesChanged
507+
})
508+
.ToListAsync(cancellationToken);
509+
510+
_logger.LogInformation(
511+
"Fetched page {Page} of commits ({Count} items, {Total} total)",
512+
page, commits.Count, totalCount);
513+
514+
var result = new PaginatedResult<object>
515+
{
516+
Items = commits,
517+
Page = page,
518+
PageSize = pageSize,
519+
TotalCount = totalCount
520+
};
521+
522+
return Ok(result);
523+
}
524+
525+
/// <summary>
526+
/// Get recent commits across all repositories for the authenticated user (legacy endpoint for dashboard)
456527
/// </summary>
457528
[HttpGet("commits/recent")]
458529
[Authorize(AuthenticationSchemes = "Bearer")]
@@ -943,29 +1014,39 @@ public async Task<IActionResult> TriggerFullSync(
9431014
}
9441015

9451016
/// <summary>
946-
/// Get all pull requests from database with optional filtering
1017+
/// Get all pull requests from database with optional filtering and pagination
9471018
/// </summary>
9481019
[HttpGet("pull-requests")]
9491020
[Authorize(AuthenticationSchemes = "Bearer")]
950-
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
1021+
[ProducesResponseType(typeof(PaginatedResult<object>), StatusCodes.Status200OK)]
9511022
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
9521023
public async Task<IActionResult> GetPullRequests(
9531024
[FromQuery] Guid? repositoryId = null,
9541025
[FromQuery] string? status = null,
1026+
[FromQuery] int page = 1,
1027+
[FromQuery] int pageSize = 25,
9551028
CancellationToken cancellationToken = default)
9561029
{
957-
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
958-
if (string.IsNullOrEmpty(userId))
1030+
if (!TryGetUserId(out var userId))
9591031
{
9601032
throw new UnauthorizedAccessException("User not authenticated");
9611033
}
9621034

1035+
// Validate and clamp pagination parameters
1036+
page = Math.Max(1, page);
1037+
pageSize = Math.Clamp(pageSize, 10, 100);
1038+
9631039
var prRepository = _unitOfWork.Repository<PullRequest>();
964-
var pullRequests = await prRepository.GetAllAsync(cancellationToken);
1040+
1041+
// Build query with filters
1042+
var query = prRepository.Query()
1043+
.Include(pr => pr.Author)
1044+
.Include(pr => pr.Repository)
1045+
.AsQueryable();
9651046

9661047
if (repositoryId.HasValue)
9671048
{
968-
pullRequests = pullRequests.Where(pr => pr.RepositoryId == repositoryId.Value);
1049+
query = query.Where(pr => pr.RepositoryId == repositoryId.Value);
9691050
}
9701051

9711052
if (!string.IsNullOrEmpty(status) && status.ToLower() != "all")
@@ -980,12 +1061,18 @@ public async Task<IActionResult> GetPullRequests(
9801061

9811062
if (prStatus.HasValue)
9821063
{
983-
pullRequests = pullRequests.Where(pr => pr.Status == prStatus.Value);
1064+
query = query.Where(pr => pr.Status == prStatus.Value);
9841065
}
9851066
}
9861067

987-
var sortedPRs = pullRequests
1068+
// Get total count before pagination
1069+
var totalCount = await query.CountAsync(cancellationToken);
1070+
1071+
// Apply pagination
1072+
var pullRequests = await query
9881073
.OrderByDescending(pr => pr.UpdatedAt)
1074+
.Skip((page - 1) * pageSize)
1075+
.Take(pageSize)
9891076
.Select(pr => new
9901077
{
9911078
id = pr.Id,
@@ -994,27 +1081,32 @@ public async Task<IActionResult> GetPullRequests(
9941081
description = pr.Description,
9951082
status = pr.Status.ToString(),
9961083
isMerged = pr.Status == PullRequestStatus.Merged,
997-
authorName = pr.Author?.DisplayName ?? "Unknown",
998-
authorUsername = pr.Author?.GitHubUsername ?? "Unknown",
999-
repositoryName = pr.Repository?.Name ?? "Unknown",
1084+
authorName = pr.Author != null ? pr.Author.DisplayName ?? "Unknown" : "Unknown",
1085+
authorUsername = pr.Author != null ? pr.Author.GitHubUsername ?? "Unknown" : "Unknown",
1086+
repositoryName = pr.Repository != null ? pr.Repository.Name ?? "Unknown" : "Unknown",
10001087
repositoryId = pr.RepositoryId,
10011088
createdAt = pr.CreatedAt,
10021089
updatedAt = pr.UpdatedAt,
10031090
closedAt = pr.ClosedAt,
10041091
mergedAt = pr.MergedAt,
1005-
url = pr.Repository?.FullName != null ?
1006-
$"https://github.com/{pr.Repository?.FullName}/pull/{pr.ExternalId}" : null
1092+
url = pr.Repository != null && pr.Repository.FullName != null ?
1093+
$"https://github.com/{pr.Repository.FullName}/pull/{pr.ExternalId}" : null
10071094
})
1008-
.ToList();
1095+
.ToListAsync(cancellationToken);
10091096

1010-
_logger.LogInformation("Fetched {Count} pull requests from database", sortedPRs.Count);
1097+
_logger.LogInformation(
1098+
"Fetched page {Page} of pull requests ({Count} items, {Total} total)",
1099+
page, pullRequests.Count, totalCount);
10111100

1012-
return Ok(new
1101+
var result = new PaginatedResult<object>
10131102
{
1014-
success = true,
1015-
count = sortedPRs.Count,
1016-
pullRequests = sortedPRs
1017-
});
1103+
Items = pullRequests,
1104+
Page = page,
1105+
PageSize = pageSize,
1106+
TotalCount = totalCount
1107+
};
1108+
1109+
return Ok(result);
10181110
}
10191111

10201112
}

0 commit comments

Comments
 (0)