[backend/api] Add PaginationWrapper<TData> & associated helper methods
This commit is contained in:
parent
55530f482d
commit
4b5d76961f
6 changed files with 101 additions and 36 deletions
|
@ -7,7 +7,11 @@ using Microsoft.AspNetCore.Mvc.Filters;
|
|||
|
||||
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 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
|
||||
{
|
||||
private const string Key = "link-pagination";
|
||||
|
|
|
@ -13,6 +13,7 @@ using Iceshrimp.Backend.Core.Helpers;
|
|||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
using Iceshrimp.Shared.Schemas.Web;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
|
@ -217,10 +218,10 @@ public class NoteController(
|
|||
[HttpGet("{id}/likes")]
|
||||
[Authenticate]
|
||||
[Authorize]
|
||||
[LinkPagination(20, 40)]
|
||||
[RestPagination(20, 40)]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[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 note = await db.Notes
|
||||
|
@ -236,8 +237,8 @@ public class NoteController(
|
|||
.Wrap(p => p.User)
|
||||
.ToListAsync();
|
||||
|
||||
HttpContext.SetPaginationData(users);
|
||||
return await userRenderer.RenderMany(users.Select(p => p.Entity));
|
||||
var res = await userRenderer.RenderMany(users.Select(p => p.Entity));
|
||||
return HttpContext.CreatePaginationWrapper(pq, users, res);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/renote")]
|
||||
|
|
50
Iceshrimp.Backend/Core/Extensions/HttpContextExtensions.cs
Normal file
50
Iceshrimp.Backend/Core/Extensions/HttpContextExtensions.cs
Normal 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;
|
||||
}
|
|
@ -276,13 +276,11 @@ public static class QueryableExtensions
|
|||
ControllerContext context
|
||||
) where T : IEntity
|
||||
{
|
||||
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter)
|
||||
.OfType<LinkPaginationAttribute>()
|
||||
.FirstOrDefault();
|
||||
if (filter == null)
|
||||
throw new Exception("Route doesn't have a LinkPaginationAttribute");
|
||||
var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<IPaginationAttribute>();
|
||||
if (attr == null)
|
||||
throw new Exception("Route doesn't have a IPaginationAttribute");
|
||||
|
||||
return Paginate(query, pq, filter.DefaultLimit, filter.MaxLimit);
|
||||
return Paginate(query, pq, attr.DefaultLimit, attr.MaxLimit);
|
||||
}
|
||||
|
||||
public static IQueryable<T> PaginateByOffset<T>(
|
||||
|
@ -304,13 +302,11 @@ public static class QueryableExtensions
|
|||
ControllerContext context
|
||||
) where T : IEntity
|
||||
{
|
||||
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter)
|
||||
.OfType<LinkPaginationAttribute>()
|
||||
.FirstOrDefault();
|
||||
if (filter == null)
|
||||
throw new Exception("Route doesn't have a LinkPaginationAttribute");
|
||||
var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<IPaginationAttribute>();
|
||||
if (attr == null)
|
||||
throw new Exception("Route doesn't have a IPaginationAttribute");
|
||||
|
||||
return PaginateByOffset(query, pq, filter.DefaultLimit, filter.MaxLimit);
|
||||
return PaginateByOffset(query, pq, attr.DefaultLimit, attr.MaxLimit);
|
||||
}
|
||||
|
||||
public static IQueryable<T> Paginate<T>(
|
||||
|
@ -320,13 +316,11 @@ public static class QueryableExtensions
|
|||
ControllerContext context
|
||||
) where T : IEntity
|
||||
{
|
||||
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter)
|
||||
.OfType<LinkPaginationAttribute>()
|
||||
.FirstOrDefault();
|
||||
if (filter == null)
|
||||
throw new Exception("Route doesn't have a LinkPaginationAttribute");
|
||||
var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<IPaginationAttribute>();
|
||||
if (attr == null)
|
||||
throw new Exception("Route doesn't have a IPaginationAttribute");
|
||||
|
||||
return Paginate(query, predicate, pq, filter.DefaultLimit, filter.MaxLimit);
|
||||
return Paginate(query, predicate, pq, attr.DefaultLimit, attr.MaxLimit);
|
||||
}
|
||||
|
||||
public static IQueryable<T> Paginate<T>(
|
||||
|
@ -336,13 +330,11 @@ public static class QueryableExtensions
|
|||
ControllerContext context
|
||||
) where T : IEntity
|
||||
{
|
||||
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter)
|
||||
.OfType<LinkPaginationAttribute>()
|
||||
.FirstOrDefault();
|
||||
if (filter == null)
|
||||
throw new Exception("Route doesn't have a LinkPaginationAttribute");
|
||||
var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<IPaginationAttribute>();
|
||||
if (attr == null)
|
||||
throw new Exception("Route doesn't have a IPaginationAttribute");
|
||||
|
||||
return Paginate(query, predicate, pq, filter.DefaultLimit, filter.MaxLimit);
|
||||
return Paginate(query, predicate, pq, attr.DefaultLimit, attr.MaxLimit);
|
||||
}
|
||||
|
||||
public static IQueryable<T> Paginate<T>(
|
||||
|
@ -351,13 +343,11 @@ public static class QueryableExtensions
|
|||
ControllerContext context
|
||||
) where T : IEntity
|
||||
{
|
||||
var filter = context.ActionDescriptor.FilterDescriptors.Select(p => p.Filter)
|
||||
.OfType<LinkPaginationAttribute>()
|
||||
.FirstOrDefault();
|
||||
if (filter == null)
|
||||
throw new Exception("Route doesn't have a LinkPaginationAttribute");
|
||||
var attr = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<IPaginationAttribute>();
|
||||
if (attr == null)
|
||||
throw new Exception("Route doesn't have a IPaginationAttribute");
|
||||
|
||||
return Paginate(query, pq, filter.DefaultLimit, filter.MaxLimit);
|
||||
return Paginate(query, pq, attr.DefaultLimit, attr.MaxLimit);
|
||||
}
|
||||
|
||||
public static IQueryable<EntityWrapper<TResult>> Wrap<TSource, TResult>(
|
||||
|
|
|
@ -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) =>
|
||||
(includeRoute ? ctx.Request.Path.ToString() + "#" : "") + (GetRateLimitPartitionInternal(ctx) ?? "");
|
||||
|
|
14
Iceshrimp.Shared/Schemas/Web/PaginationWrapper.cs
Normal file
14
Iceshrimp.Shared/Schemas/Web/PaginationWrapper.cs
Normal 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; }
|
||||
}
|
Loading…
Add table
Reference in a new issue