[backend/masto-client] Correctly handle offset pagination (ISH-358)

This commit is contained in:
Laura Hausmann 2024-06-07 18:33:54 +02:00
parent fc2dcb5e97
commit c46a5bd93d
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
3 changed files with 44 additions and 6 deletions

View file

@ -1,3 +1,4 @@
using Iceshrimp.Backend.Controllers.Mastodon.Schemas;
using Iceshrimp.Backend.Core.Database;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
@ -5,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
namespace Iceshrimp.Backend.Controllers.Attributes;
public class LinkPaginationAttribute(int defaultLimit, int maxLimit) : ActionFilterAttribute
public class LinkPaginationAttribute(int defaultLimit, int maxLimit, bool offset = false) : ActionFilterAttribute
{
public int DefaultLimit => defaultLimit;
public int MaxLimit => maxLimit;
@ -39,14 +40,23 @@ public class LinkPaginationAttribute(int defaultLimit, int maxLimit) : ActionFil
var limit = Math.Min(query.Limit ?? defaultLimit, maxLimit);
var request = context.HttpContext.Request;
var mpq = query as MastodonPaginationQuery;
var offsetPg = offset || mpq is { Offset: not null, MaxId: null, MinId: null, SinceId: null };
if (ids.Count >= limit)
{
var next = new QueryBuilder { { "limit", limit.ToString() }, { "max_id", ids.Last() } };
var next = offsetPg
? new QueryBuilder { { "offset", ((mpq?.Offset ?? 0) + limit).ToString() } }
: new QueryBuilder { { "limit", limit.ToString() }, { "max_id", ids.Last() } };
links.Add($"<{GetUrl(request, next.ToQueryString())}>; rel=\"next\"");
}
var prev = new QueryBuilder { { "limit", limit.ToString() }, { "min_id", ids.First() } };
links.Add($"<{GetUrl(request, prev.ToQueryString())}>; rel=\"prev\"");
var prev = offsetPg
? new QueryBuilder { { "offset", Math.Max(0, (mpq?.Offset ?? 0) - limit).ToString() } }
: new QueryBuilder { { "limit", limit.ToString() }, { "min_id", ids.First() } };
if (!offsetPg || (mpq?.Offset ?? 0) != 0)
links.Add($"<{GetUrl(request, prev.ToQueryString())}>; rel=\"prev\"");
context.HttpContext.Response.Headers.Link = string.Join(", ", links);
}

View file

@ -56,7 +56,7 @@ public class SearchController(
[HttpGet("/api/v1/accounts/search")]
[Authorize("read:accounts")]
[LinkPagination(20, 40)]
[LinkPagination(20, 40, true)]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<AccountEntity>))]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))]
public async Task<IActionResult> SearchAccounts(
@ -133,8 +133,8 @@ public class SearchController(
.Where(p => p.DisplayNameOrUsernameOrFqnContainsCaseInsensitive(search.Query!,
config.Value.AccountDomain))
.Where(p => !search.Following || p.IsFollowedBy(user))
.Paginate(pagination, ControllerContext) //TODO: this will mess up our sorting
.OrderByDescending(p => p.NotesCount)
.PaginateByOffset(pagination, ControllerContext)
.RenderAllForMastodonAsync(userRenderer);
}

View file

@ -179,6 +179,34 @@ public static class QueryableExtensions
return Paginate(query, pq, filter.DefaultLimit, filter.MaxLimit);
}
public static IQueryable<T> PaginateByOffset<T>(
this IQueryable<T> query,
MastodonPaginationQuery pq,
int defaultLimit,
int maxLimit
) where T : IEntity
{
if (pq.Limit is < 1)
throw GracefulException.BadRequest("Limit cannot be less than 1");
return query.Skip(pq.Offset ?? 0).Take(Math.Min(pq.Limit ?? defaultLimit, maxLimit));
}
public static IQueryable<T> PaginateByOffset<T>(
this IQueryable<T> query,
MastodonPaginationQuery pq,
ControllerContext context
) where T : IEntity
{
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter)
.OfType<LinkPaginationAttribute>()
.FirstOrDefault();
if (filter == null)
throw new GracefulException("Route doesn't have a LinkPaginationAttribute");
return PaginateByOffset(query, pq, filter.DefaultLimit, filter.MaxLimit);
}
public static IQueryable<T> Paginate<T>(
this IQueryable<T> query,
Expression<Func<T, string>> predicate,