Skip to content

Commit 5f1129e

Browse files
feat: create pagination UI component (#1579)
* feat: create pagination UI component * feat: changed from cursor base to page based pagination, dynamic endpoint * feat: navigation headers and response refactor * feat: SonarQube - url.match replaced with Regex.exec * refactor: break functions out to reduce complexity * chore: file format * chore: file format * test: refactored tests to new implementation * fix: fix the mock route for local testing of exceptions * fix: formatting and pass exceptionStatus * fix: use the correct UpdateExceptionServiceNowId endpoint * fix: fix the data object and ruleMapping update tests * feat: split out pagination logic and use on raised exceptions * fix: refactor the pagination logic * fix: remove extra whitespace * chore: formatting * test: isReportTests * fix: reduce complexity of pagination functions * fix: fix nested template literals * test: tests added, fluent assertions added to csproj, loggerAssertions updated * chore: constant assignment of magic string --------- Co-authored-by: warren <warren.pitterson1@nhs.net> Co-authored-by: Warren-Pitterson <168638743+Warren-Pitterson@users.noreply.github.com>
1 parent fa73947 commit 5f1129e

21 files changed

Lines changed: 1506 additions & 424 deletions

File tree

application/CohortManager/src/Functions/Shared/Common/Pagination/IPaginationService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ namespace Common;
44

55
public interface IPaginationService<T>
66
{
7-
PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int? lastId, Func<T, int>? idSelector = null);
7+
PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int page = 1);
88
Dictionary<string, string> AddNavigationHeaders<TEntity>(HttpRequestData request, PaginationResult<TEntity> paginationResult);
99
}

application/CohortManager/src/Functions/Shared/Common/Pagination/PaginationService.cs

Lines changed: 47 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,36 @@ public class PaginationService<T> : IPaginationService<T>
66
{
77
private const int pageSize = 10;
88

9-
public PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int? lastId, Func<T, int>? idSelector = null)
9+
/// <summary>
10+
/// Gets paginated results using page-based pagination
11+
/// </summary>
12+
/// <param name="source">The queryable source</param>
13+
/// <param name="page">The page number (1-based)</param>
14+
/// <returns>Paginated result</returns>
15+
public PaginationResult<T> GetPaginatedResult(IQueryable<T> source, int page = 1)
1016
{
11-
// If no idSelector is provided, try to use a default 'Id' property
12-
if (idSelector == null)
13-
{
14-
idSelector = GetDefaultIdSelector();
15-
}
16-
17-
// Convert source to a list of IDs to calculate index-based pagination
18-
var idList = source.Select(idSelector).OrderBy(id => id).ToList();
19-
20-
// Get the index of the lastId
21-
int lastIdIndex = lastId.HasValue ? idList.IndexOf(lastId.Value) : -1;
22-
int currentPage = lastIdIndex >= 0 ? (lastIdIndex / pageSize) + 2 : 1;
2317
var totalItems = source.Count();
2418
var totalPages = (int)Math.Ceiling((double)totalItems / pageSize);
2519

26-
if (lastIdIndex >= 0)
27-
{
28-
source = source.Skip(lastIdIndex + 1);
29-
}
20+
// Ensure page is within valid range
21+
page = Math.Max(1, Math.Min(page, Math.Max(1, totalPages)));
3022

31-
var items = source.Take(pageSize).ToList();
32-
int? lastResultId = items.Count > 0 ? idSelector(items[^1]) : null;
23+
var items = source
24+
.Skip((page - 1) * pageSize)
25+
.Take(pageSize)
26+
.ToList();
3327

3428
return new PaginationResult<T>
3529
{
3630
Items = items,
37-
IsFirstPage = currentPage == 1,
38-
HasNextPage = lastResultId.HasValue && (lastIdIndex + pageSize < totalItems),
39-
LastResultId = lastResultId,
31+
IsFirstPage = page == 1,
32+
HasNextPage = page < totalPages,
33+
HasPreviousPage = page > 1,
34+
LastResultId = null, // Not used in page-based pagination
4035
TotalItems = totalItems,
4136
TotalPages = totalPages,
42-
CurrentPage = currentPage,
37+
CurrentPage = page,
38+
PageSize = pageSize
4339
};
4440
}
4541

@@ -52,14 +48,12 @@ public Dictionary<string, string> AddNavigationHeaders<TEntity>(HttpRequestData
5248
{
5349
["X-Total-Count"] = paginationResult.TotalItems.ToString(),
5450
["X-Has-Next-Page"] = paginationResult.HasNextPage.ToString().ToLower(),
55-
["X-Is-First-Page"] = paginationResult.IsFirstPage.ToString().ToLower()
51+
["X-Has-Previous-Page"] = paginationResult.HasPreviousPage.ToString().ToLower(),
52+
["X-Is-First-Page"] = paginationResult.IsFirstPage.ToString().ToLower(),
53+
["X-Current-Page"] = paginationResult.CurrentPage.ToString(),
54+
["X-Total-Pages"] = paginationResult.TotalPages.ToString()
5655
};
5756

58-
if (paginationResult.LastResultId.HasValue)
59-
{
60-
headers["X-Last-Id"] = paginationResult.LastResultId.Value.ToString();
61-
}
62-
6357
var linkHeaders = BuildLinkHeaders(request, paginationResult);
6458
if (linkHeaders.Count > 0)
6559
{
@@ -74,45 +68,47 @@ private static List<string> BuildLinkHeaders<TEntity>(HttpRequestData request, P
7468
var linkHeaders = new List<string>();
7569
var baseUrl = request.Url.GetLeftPart(UriPartial.Path);
7670
var queryString = request.Url.Query;
77-
var baseQuery = RemoveLastIdParam(queryString);
71+
var baseQuery = RemovePageParam(queryString);
7872
var separator = string.IsNullOrEmpty(baseQuery) ? "?" : "&";
7973

80-
// First page link (no lastId)
74+
// First page link
8175
linkHeaders.Add($"<{baseUrl}{baseQuery}>; rel=\"first\"");
8276

83-
// Next page link (only if has next page)
84-
if (paginationResult.HasNextPage && paginationResult.LastResultId.HasValue)
77+
// Previous page link
78+
if (paginationResult.HasPreviousPage)
8579
{
86-
linkHeaders.Add($"<{baseUrl}{baseQuery}{separator}lastId={paginationResult.LastResultId.Value}>; rel=\"next\"");
80+
var prevPage = paginationResult.CurrentPage - 1;
81+
var prevUrl = prevPage == 1
82+
? $"{baseUrl}{baseQuery}"
83+
: $"{baseUrl}{baseQuery}{separator}page={prevPage}";
84+
linkHeaders.Add($"<{prevUrl}>; rel=\"prev\"");
85+
}
86+
87+
// Next page link
88+
if (paginationResult.HasNextPage)
89+
{
90+
var nextPage = paginationResult.CurrentPage + 1;
91+
linkHeaders.Add($"<{baseUrl}{baseQuery}{separator}page={nextPage}>; rel=\"next\"");
92+
}
93+
94+
// Last page link
95+
if (paginationResult.TotalPages > 1)
96+
{
97+
linkHeaders.Add($"<{baseUrl}{baseQuery}{separator}page={paginationResult.TotalPages}>; rel=\"last\"");
8798
}
8899

89100
return linkHeaders;
90101
}
91102

92-
private static string RemoveLastIdParam(string queryString)
103+
private static string RemovePageParam(string queryString)
93104
{
94105
if (string.IsNullOrEmpty(queryString)) return "";
95106

96107
var pairs = queryString.TrimStart('?').Split('&')
97-
.Where(p => !p.StartsWith("lastId="))
108+
.Where(p => !p.StartsWith("page="))
98109
.Where(p => !string.IsNullOrWhiteSpace(p))
99110
.ToArray();
100111

101112
return pairs.Length > 0 ? "?" + string.Join("&", pairs) : "";
102113
}
103-
104-
private static Func<T, int> GetDefaultIdSelector()
105-
{
106-
var idProperty = (typeof(T).GetProperty("Id") ??
107-
typeof(T).GetProperty($"{typeof(T).Name}Id")) ??
108-
throw new InvalidOperationException(
109-
"Could not find a default ID property. Provide a custom ID selector.");
110-
111-
return x =>
112-
{
113-
var value = idProperty.GetValue(x) ?? throw new InvalidOperationException(
114-
$"Entity of type {typeof(T).Name} has a null ID property ('{idProperty.Name}').");
115-
return (int)value;
116-
};
117-
}
118114
}

application/CohortManager/src/Functions/screeningDataServices/GetValidationExceptions/GetValidationExceptions.cs

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public GetValidationExceptions(ILogger<GetValidationExceptions> logger, ICreateR
4646
public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
4747
{
4848
var exceptionId = _httpParserHelper.GetQueryParameterAsInt(req, "exceptionId");
49-
var lastId = _httpParserHelper.GetQueryParameterAsInt(req, "lastId");
49+
var page = _httpParserHelper.GetQueryParameterAsInt(req, "page");
50+
if (page <= 0) page = 1;
5051
var exceptionStatus = HttpParserHelper.GetEnumQueryParameter(req, "exceptionStatus", ExceptionStatus.All);
5152
var sortOrder = HttpParserHelper.GetEnumQueryParameter(req, "sortOrder", SortOrder.Descending);
5253
var exceptionCategory = HttpParserHelper.GetEnumQueryParameter(req, "exceptionCategory", ExceptionCategory.NBO);
@@ -71,11 +72,11 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou
7172
}
7273

7374
var reportExceptions = await _validationData.GetReportExceptions(reportDate, exceptionCategory);
74-
return CreatePaginatedResponse(req, reportExceptions, lastId);
75+
return CreatePaginatedResponse(req, reportExceptions!.AsQueryable(), page);
7576
}
7677

7778
var allExceptions = await _validationData.GetAllFilteredExceptions(exceptionStatus, sortOrder, exceptionCategory);
78-
return CreatePaginatedResponse(req, allExceptions, lastId);
79+
return CreatePaginatedResponse(req, allExceptions!.AsQueryable(), page);
7980
}
8081
catch (Exception ex)
8182
{
@@ -84,23 +85,12 @@ public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymou
8485
}
8586
}
8687

87-
88-
private HttpResponseData CreatePaginatedResponse(HttpRequestData req, List<ValidationException>? exceptions, int lastId)
88+
private HttpResponseData CreatePaginatedResponse(HttpRequestData request, IQueryable<ValidationException> source, int page)
8989
{
90-
if (exceptions == null || exceptions.Count == 0)
91-
{
92-
return _createResponse.CreateHttpResponse(HttpStatusCode.NoContent, req);
93-
}
94-
95-
var paginatedResult = _paginationService.GetPaginatedResult(exceptions.AsQueryable(), lastId == 0 ? null : lastId, e => e.ExceptionId);
96-
97-
if (!paginatedResult.Items.Any())
98-
{
99-
return _createResponse.CreateHttpResponse(HttpStatusCode.NoContent, req);
100-
}
90+
var paginatedResult = _paginationService.GetPaginatedResult(source, page);
91+
var headers = _paginationService.AddNavigationHeaders(request, paginatedResult);
10192

102-
var headers = _paginationService.AddNavigationHeaders(req, paginatedResult);
103-
return _createResponse.CreateHttpResponseWithHeaders(HttpStatusCode.OK, req, JsonSerializer.Serialize(paginatedResult), headers);
93+
return _createResponse.CreateHttpResponseWithHeaders(HttpStatusCode.OK, request, JsonSerializer.Serialize(paginatedResult), headers);
10494
}
10595

10696
/// <summary>

0 commit comments

Comments
 (0)