[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;
|
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";
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
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
|
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>(
|
||||||
|
|
|
@ -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) ?? "");
|
||||||
|
|
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