[backend/api] Add PaginationWrapper<TData> & associated helper methods

This commit is contained in:
Laura Hausmann 2024-10-14 22:25:03 +02:00 committed by Lilian
parent 55530f482d
commit 4b5d76961f
No known key found for this signature in database
6 changed files with 101 additions and 36 deletions

View file

@ -7,7 +7,11 @@ using Microsoft.AspNetCore.Mvc.Filters;
namespace Iceshrimp.Backend.Controllers.Shared.Attributes; namespace Iceshrimp.Backend.Controllers.Shared.Attributes;
public class LinkPaginationAttribute(int defaultLimit, int maxLimit, bool offset = false) : ActionFilterAttribute public class LinkPaginationAttribute(
int defaultLimit,
int maxLimit,
bool offset = false
) : ActionFilterAttribute, IPaginationAttribute
{ {
public int DefaultLimit => defaultLimit; public int DefaultLimit => defaultLimit;
public int MaxLimit => maxLimit; public int MaxLimit => maxLimit;
@ -75,6 +79,12 @@ public class LinkPaginationAttribute(int defaultLimit, int maxLimit, bool offset
} }
} }
public interface IPaginationAttribute
{
public int DefaultLimit { get; }
public int MaxLimit { get; }
}
public static class HttpContextExtensions public static class HttpContextExtensions
{ {
private const string Key = "link-pagination"; private const string Key = "link-pagination";

View file

@ -13,6 +13,7 @@ using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Services; using Iceshrimp.Backend.Core.Services;
using Iceshrimp.Shared.Schemas.Web; using Iceshrimp.Shared.Schemas.Web;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
@ -217,10 +218,10 @@ public class NoteController(
[HttpGet("{id}/likes")] [HttpGet("{id}/likes")]
[Authenticate] [Authenticate]
[Authorize] [Authorize]
[LinkPagination(20, 40)] [RestPagination(20, 40)]
[ProducesResults(HttpStatusCode.OK)] [ProducesResults(HttpStatusCode.OK)]
[ProducesErrors(HttpStatusCode.NotFound)] [ProducesErrors(HttpStatusCode.NotFound)]
public async Task<IEnumerable<UserResponse>> GetNoteLikes(string id, PaginationQuery pq) public async Task<PaginationWrapper<IEnumerable<UserResponse>>> GetNoteLikes(string id, PaginationQuery pq)
{ {
var user = HttpContext.GetUser(); var user = HttpContext.GetUser();
var note = await db.Notes var note = await db.Notes
@ -236,8 +237,8 @@ public class NoteController(
.Wrap(p => p.User) .Wrap(p => p.User)
.ToListAsync(); .ToListAsync();
HttpContext.SetPaginationData(users); var res = await userRenderer.RenderMany(users.Select(p => p.Entity));
return await userRenderer.RenderMany(users.Select(p => p.Entity)); return HttpContext.CreatePaginationWrapper(pq, users, res);
} }
[HttpPost("{id}/renote")] [HttpPost("{id}/renote")]

View file

@ -0,0 +1,50 @@
using Iceshrimp.Backend.Controllers.Shared.Attributes;
using Iceshrimp.Backend.Controllers.Shared.Schemas;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Shared.Schemas.Web;
using Microsoft.AspNetCore.Http.Extensions;
namespace Iceshrimp.Backend.Core.Extensions;
public static partial class HttpContextExtensions
{
public static PaginationWrapper<TData> CreatePaginationWrapper<TData>(
this HttpContext ctx, PaginationQuery query, IEnumerable<IEntity> paginationData, TData data
)
{
var attr = ctx.GetEndpoint()?.Metadata.GetMetadata<RestPaginationAttribute>();
if (attr == null) throw new Exception("Route doesn't have a RestPaginationAttribute");
var limit = Math.Min(query.Limit ?? attr.DefaultLimit, attr.MaxLimit);
if (limit < 1) throw GracefulException.BadRequest("Limit cannot be less than 1");
var ids = paginationData.Select(p => p.Id).ToList();
if (query.MinId != null) ids.Reverse();
var next = ids.Count >= limit ? new QueryBuilder { { "max_id", ids.Last() } } : null;
var prev = ids.Count > 0 ? new QueryBuilder { { "min_id", ids.First() } } : null;
var links = new PaginationData
{
Limit = limit,
Next = next?.ToQueryString().ToString(),
Prev = prev?.ToQueryString().ToString()
};
return new PaginationWrapper<TData> { Data = data, Links = links };
}
public static PaginationWrapper<TData> CreatePaginationWrapper<TData>(
this HttpContext ctx, PaginationQuery query, TData data
) where TData : IEnumerable<IEntity>
{
return CreatePaginationWrapper(ctx, query, data, data);
}
}
public class RestPaginationAttribute(int defaultLimit, int maxLimit) : Attribute, IPaginationAttribute
{
public int DefaultLimit => defaultLimit;
public int MaxLimit => maxLimit;
}

View file

@ -276,13 +276,11 @@ public static class QueryableExtensions
ControllerContext context ControllerContext context
) where T : IEntity ) where T : IEntity
{ {
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<IPaginationAttribute>();
.OfType<LinkPaginationAttribute>() if (attr == null)
.FirstOrDefault(); throw new Exception("Route doesn't have a IPaginationAttribute");
if (filter == null)
throw new Exception("Route doesn't have a LinkPaginationAttribute");
return Paginate(query, pq, filter.DefaultLimit, filter.MaxLimit); return Paginate(query, pq, attr.DefaultLimit, attr.MaxLimit);
} }
public static IQueryable<T> PaginateByOffset<T>( public static IQueryable<T> PaginateByOffset<T>(
@ -304,13 +302,11 @@ public static class QueryableExtensions
ControllerContext context ControllerContext context
) where T : IEntity ) where T : IEntity
{ {
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<IPaginationAttribute>();
.OfType<LinkPaginationAttribute>() if (attr == null)
.FirstOrDefault(); throw new Exception("Route doesn't have a IPaginationAttribute");
if (filter == null)
throw new Exception("Route doesn't have a LinkPaginationAttribute");
return PaginateByOffset(query, pq, filter.DefaultLimit, filter.MaxLimit); return PaginateByOffset(query, pq, attr.DefaultLimit, attr.MaxLimit);
} }
public static IQueryable<T> Paginate<T>( public static IQueryable<T> Paginate<T>(
@ -320,13 +316,11 @@ public static class QueryableExtensions
ControllerContext context ControllerContext context
) where T : IEntity ) where T : IEntity
{ {
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<IPaginationAttribute>();
.OfType<LinkPaginationAttribute>() if (attr == null)
.FirstOrDefault(); throw new Exception("Route doesn't have a IPaginationAttribute");
if (filter == null)
throw new Exception("Route doesn't have a LinkPaginationAttribute");
return Paginate(query, predicate, pq, filter.DefaultLimit, filter.MaxLimit); return Paginate(query, predicate, pq, attr.DefaultLimit, attr.MaxLimit);
} }
public static IQueryable<T> Paginate<T>( public static IQueryable<T> Paginate<T>(
@ -336,13 +330,11 @@ public static class QueryableExtensions
ControllerContext context ControllerContext context
) where T : IEntity ) where T : IEntity
{ {
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<IPaginationAttribute>();
.OfType<LinkPaginationAttribute>() if (attr == null)
.FirstOrDefault(); throw new Exception("Route doesn't have a IPaginationAttribute");
if (filter == null)
throw new Exception("Route doesn't have a LinkPaginationAttribute");
return Paginate(query, predicate, pq, filter.DefaultLimit, filter.MaxLimit); return Paginate(query, predicate, pq, attr.DefaultLimit, attr.MaxLimit);
} }
public static IQueryable<T> Paginate<T>( public static IQueryable<T> Paginate<T>(
@ -351,13 +343,11 @@ public static class QueryableExtensions
ControllerContext context ControllerContext context
) where T : IEntity ) where T : IEntity
{ {
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter) var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<IPaginationAttribute>();
.OfType<LinkPaginationAttribute>() if (attr == null)
.FirstOrDefault(); throw new Exception("Route doesn't have a IPaginationAttribute");
if (filter == null)
throw new Exception("Route doesn't have a LinkPaginationAttribute");
return Paginate(query, pq, filter.DefaultLimit, filter.MaxLimit); return Paginate(query, pq, attr.DefaultLimit, attr.MaxLimit);
} }
public static IQueryable<EntityWrapper<TResult>> Wrap<TSource, TResult>( public static IQueryable<EntityWrapper<TResult>> Wrap<TSource, TResult>(

View file

@ -365,7 +365,7 @@ public static class ServiceExtensions
} }
} }
public static class HttpContextExtensions public static partial class HttpContextExtensions
{ {
public static string GetRateLimitPartition(this HttpContext ctx, bool includeRoute) => public static string GetRateLimitPartition(this HttpContext ctx, bool includeRoute) =>
(includeRoute ? ctx.Request.Path.ToString() + "#" : "") + (GetRateLimitPartitionInternal(ctx) ?? ""); (includeRoute ? ctx.Request.Path.ToString() + "#" : "") + (GetRateLimitPartitionInternal(ctx) ?? "");

View file

@ -0,0 +1,14 @@
namespace Iceshrimp.Shared.Schemas.Web;
public class PaginationWrapper<TData>
{
public required PaginationData Links { get; set; }
public required TData Data { get; set; }
}
public class PaginationData
{
public required int Limit { get; set; }
public string? Next { get; set; }
public string? Prev { get; set; }
}