From befaa207d7ee5d42f5697c0fd4e9b5a9c8f4616d Mon Sep 17 00:00:00 2001 From: john Date: Sun, 4 May 2025 00:57:27 +0200 Subject: [PATCH] stuff --- .../Controllers/Authors/AuthorsController.cs | 26 +++-- .../Authors/Dto/AuthoPostAuthorDto.cs | 6 ++ .../Controllers/Authors/Dto/AuthorPostDto.cs | 6 ++ .../Authors/Dto/GetAuthorPostsResponse.cs | 5 +- .../Authors/Dto/GetAuthorPostsSearchParams.cs | 3 + .../Authors/GetAuthorPostsSearchParams.cs | 3 - .../Posts/Dto/GetAllPublicPostsResponse.cs | 6 ++ .../Controllers/Posts/Dto/GetPostResponse.cs | 6 -- .../Posts/Dto/GetPublicPostsSearchParams.cs | 6 ++ .../Posts/Dto/PublicPostAuthorDto.cs | 6 ++ .../Controllers/Posts/Dto/PublicPostDto.cs | 12 +++ .../Controllers/Posts/PostsController.cs | 30 +++++- Femto.Api/Program.cs | 12 +++ Femto.Api/Properties/launchSettings.json | 4 +- .../Dto/GetAuthorPostsDto.cs | 5 - .../GetAuthorPosts/GetAuthorPostsQuery.cs | 6 -- .../GetAuthorPostsQueryHandler.cs | 61 ------------ .../GetPosts/Dto/GetPostsQueryResult.cs | 3 + .../Commands/GetPosts/Dto/PostAuthorDto.cs | 3 + .../Posts/Commands/GetPosts/Dto/PostDto.cs | 3 + .../Commands/GetPosts/Dto/PostMediaDto.cs | 3 + .../Posts/Commands/GetPosts/GetPostsQuery.cs | 25 +++++ .../Commands/GetPosts/GetPostsQueryHandler.cs | 99 +++++++++++++++++++ 23 files changed, 244 insertions(+), 95 deletions(-) create mode 100644 Femto.Api/Controllers/Authors/Dto/AuthoPostAuthorDto.cs create mode 100644 Femto.Api/Controllers/Authors/Dto/AuthorPostDto.cs create mode 100644 Femto.Api/Controllers/Authors/Dto/GetAuthorPostsSearchParams.cs delete mode 100644 Femto.Api/Controllers/Authors/GetAuthorPostsSearchParams.cs create mode 100644 Femto.Api/Controllers/Posts/Dto/GetAllPublicPostsResponse.cs delete mode 100644 Femto.Api/Controllers/Posts/Dto/GetPostResponse.cs create mode 100644 Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs create mode 100644 Femto.Api/Controllers/Posts/Dto/PublicPostAuthorDto.cs create mode 100644 Femto.Api/Controllers/Posts/Dto/PublicPostDto.cs delete mode 100644 Femto.Modules.Blog.Contracts/Dto/GetAuthorPostsDto.cs delete mode 100644 Femto.Modules.Blog/Domain/Posts/Commands/GetAuthorPosts/GetAuthorPostsQuery.cs delete mode 100644 Femto.Modules.Blog/Domain/Posts/Commands/GetAuthorPosts/GetAuthorPostsQueryHandler.cs create mode 100644 Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/GetPostsQueryResult.cs create mode 100644 Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostAuthorDto.cs create mode 100644 Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostDto.cs create mode 100644 Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostMediaDto.cs create mode 100644 Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQuery.cs create mode 100644 Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQueryHandler.cs diff --git a/Femto.Api/Controllers/Authors/AuthorsController.cs b/Femto.Api/Controllers/Authors/AuthorsController.cs index 498829b..8fec530 100644 --- a/Femto.Api/Controllers/Authors/AuthorsController.cs +++ b/Femto.Api/Controllers/Authors/AuthorsController.cs @@ -1,5 +1,5 @@ using Femto.Api.Controllers.Authors.Dto; -using Femto.Modules.Blog.Domain.Posts.Commands.GetAuthorPosts; +using Femto.Modules.Blog.Domain.Posts.Commands.GetPosts; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -8,21 +8,33 @@ namespace Femto.Api.Controllers.Authors; [ApiController] [Route("authors")] public class AuthorsController(IMediator mediator) : ControllerBase -{ - [HttpGet("{authorId}/posts")] +{ + [HttpGet("{username}/posts")] public async Task> GetAuthorPosts( - Guid authorId, + string username, [FromQuery] GetAuthorPostsSearchParams searchParams, CancellationToken cancellationToken ) { - var posts = await mediator.Send( - new GetAuthorPostsQuery(authorId, searchParams.Cursor, searchParams.Count), + var res = await mediator.Send( + new GetPostsQuery + { + Username = username, + Amount = searchParams.Amount ?? 20, + From = searchParams.From, + }, cancellationToken ); return new GetAuthorPostsResponse( - posts.Select(p => new AuthorPostDto(p.PostId, p.Text, p.Media.Select(m => m.Url))) + res.Posts.Select(p => new AuthorPostDto( + p.PostId, + p.Text, + p.Media.Select(m => m.Url), + p.CreatedAt, + new AuthoPostAuthorDto(p.Author.AuthorId, p.Author.Username) + )), + res.Next ); } } diff --git a/Femto.Api/Controllers/Authors/Dto/AuthoPostAuthorDto.cs b/Femto.Api/Controllers/Authors/Dto/AuthoPostAuthorDto.cs new file mode 100644 index 0000000..d6e4f32 --- /dev/null +++ b/Femto.Api/Controllers/Authors/Dto/AuthoPostAuthorDto.cs @@ -0,0 +1,6 @@ +using JetBrains.Annotations; + +namespace Femto.Api.Controllers.Authors.Dto; + +[PublicAPI] +public record AuthoPostAuthorDto(Guid AuthorId, string Username); \ No newline at end of file diff --git a/Femto.Api/Controllers/Authors/Dto/AuthorPostDto.cs b/Femto.Api/Controllers/Authors/Dto/AuthorPostDto.cs new file mode 100644 index 0000000..20837dc --- /dev/null +++ b/Femto.Api/Controllers/Authors/Dto/AuthorPostDto.cs @@ -0,0 +1,6 @@ +using JetBrains.Annotations; + +namespace Femto.Api.Controllers.Authors.Dto; + +[PublicAPI] +public record AuthorPostDto(Guid PostId, string Content, IEnumerable Media, DateTime CreatedAt, AuthoPostAuthorDto Author ); \ No newline at end of file diff --git a/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsResponse.cs b/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsResponse.cs index 6485d45..10af47a 100644 --- a/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsResponse.cs +++ b/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsResponse.cs @@ -3,7 +3,4 @@ using JetBrains.Annotations; namespace Femto.Api.Controllers.Authors.Dto; [PublicAPI] -public record GetAuthorPostsResponse(IEnumerable Posts); - -[PublicAPI] -public record AuthorPostDto(Guid PostId, string Content, IEnumerable Media); +public record GetAuthorPostsResponse(IEnumerable Posts, Guid? Next); \ No newline at end of file diff --git a/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsSearchParams.cs b/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsSearchParams.cs new file mode 100644 index 0000000..762615d --- /dev/null +++ b/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsSearchParams.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Authors; + +public record GetAuthorPostsSearchParams(Guid? From, int? Amount); \ No newline at end of file diff --git a/Femto.Api/Controllers/Authors/GetAuthorPostsSearchParams.cs b/Femto.Api/Controllers/Authors/GetAuthorPostsSearchParams.cs deleted file mode 100644 index cac1d91..0000000 --- a/Femto.Api/Controllers/Authors/GetAuthorPostsSearchParams.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Femto.Api.Controllers.Authors; - -public record GetAuthorPostsSearchParams(Guid? Cursor, int? Count); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/GetAllPublicPostsResponse.cs b/Femto.Api/Controllers/Posts/Dto/GetAllPublicPostsResponse.cs new file mode 100644 index 0000000..535f4af --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/GetAllPublicPostsResponse.cs @@ -0,0 +1,6 @@ +using JetBrains.Annotations; + +namespace Femto.Api.Controllers.Posts.Dto; + +[PublicAPI] +public record GetAllPublicPostsResponse(IEnumerable Posts, Guid? Next); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/GetPostResponse.cs b/Femto.Api/Controllers/Posts/Dto/GetPostResponse.cs deleted file mode 100644 index 757afd0..0000000 --- a/Femto.Api/Controllers/Posts/Dto/GetPostResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Femto.Api.Controllers.Posts.Dto; - -public record GetPostResponse -{ - -} \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs b/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs new file mode 100644 index 0000000..228626c --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs @@ -0,0 +1,6 @@ +using JetBrains.Annotations; + +namespace Femto.Api.Controllers.Posts.Dto; + +[PublicAPI] +public record GetPublicPostsSearchParams(Guid? From, int? Amount); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/PublicPostAuthorDto.cs b/Femto.Api/Controllers/Posts/Dto/PublicPostAuthorDto.cs new file mode 100644 index 0000000..d9931da --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/PublicPostAuthorDto.cs @@ -0,0 +1,6 @@ +using JetBrains.Annotations; + +namespace Femto.Api.Controllers.Posts.Dto; + +[PublicAPI] +public record PublicPostAuthorDto(Guid AuthorId, string Username); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/PublicPostDto.cs b/Femto.Api/Controllers/Posts/Dto/PublicPostDto.cs new file mode 100644 index 0000000..35e7dfc --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/PublicPostDto.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; + +namespace Femto.Api.Controllers.Posts.Dto; + +[PublicAPI] +public record PublicPostDto( + PublicPostAuthorDto Author, + Guid PostId, + string Content, + IEnumerable Media, + DateTime CreatedAt +); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/PostsController.cs b/Femto.Api/Controllers/Posts/PostsController.cs index 88087b3..12db869 100644 --- a/Femto.Api/Controllers/Posts/PostsController.cs +++ b/Femto.Api/Controllers/Posts/PostsController.cs @@ -1,5 +1,6 @@ using Femto.Api.Controllers.Posts.Dto; using Femto.Modules.Blog.Domain.Posts.Commands.CreatePost; +using Femto.Modules.Blog.Domain.Posts.Commands.GetPosts; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -9,6 +10,33 @@ namespace Femto.Api.Controllers.Posts; [Route("posts")] public class PostsController(IMediator mediator) : ControllerBase { + [HttpGet] + public async Task> GetAllPublicPosts( + [FromQuery] GetPublicPostsSearchParams searchParams, + CancellationToken cancellationToken + ) + { + var res = await mediator.Send( + new GetPostsQuery + { + From = searchParams.From, + Amount = searchParams.Amount ?? 20 + }, + cancellationToken + ); + + return new GetAllPublicPostsResponse( + res.Posts.Select(p => new PublicPostDto( + new PublicPostAuthorDto(p.Author.AuthorId, p.Author.Username), + p.PostId, + p.Text, + p.Media.Select(m => m.Url), + p.CreatedAt + )), + res.Next + ); + } + [HttpPost] public async Task> Post( [FromBody] CreatePostRequest req, @@ -19,7 +47,7 @@ public class PostsController(IMediator mediator) : ControllerBase new CreatePostCommand(req.AuthorId, req.Content, req.Media), cancellationToken ); - + return new CreatePostResponse(guid); } } diff --git a/Femto.Api/Program.cs b/Femto.Api/Program.cs index d22eea5..603620a 100644 --- a/Femto.Api/Program.cs +++ b/Femto.Api/Program.cs @@ -14,6 +14,16 @@ builder.Services.UseBlogModule(databaseConnectionString); builder.Services.AddControllers(); +builder.Services.AddCors(options => +{ + options.AddPolicy( + "DefaultCorsPolicy", + b => + { + b.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin(); + } + ); +}); builder .Services.AddControllers() .AddJsonOptions(options => @@ -27,6 +37,8 @@ builder var app = builder.Build(); +app.UseCors("DefaultCorsPolicy"); + if (app.Environment.IsDevelopment()) { app.MapOpenApi(); diff --git a/Femto.Api/Properties/launchSettings.json b/Femto.Api/Properties/launchSettings.json index ba9387f..237dc27 100644 --- a/Femto.Api/Properties/launchSettings.json +++ b/Femto.Api/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "http://localhost:5181", + "applicationUrl": "http://0.0.0.0:5181", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -14,7 +14,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "applicationUrl": "https://localhost:7269;http://localhost:5181", + "applicationUrl": "https://0.0.0.0:7269;http://0.0.0.0:5181", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Femto.Modules.Blog.Contracts/Dto/GetAuthorPostsDto.cs b/Femto.Modules.Blog.Contracts/Dto/GetAuthorPostsDto.cs deleted file mode 100644 index 649b4be..0000000 --- a/Femto.Modules.Blog.Contracts/Dto/GetAuthorPostsDto.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Femto.Modules.Blog.Contracts.Dto; - -public record GetAuthorPostsDto(Guid PostId, string Text, IList Media); - -public record GetAuthorPostsMediaDto(Uri Url); \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetAuthorPosts/GetAuthorPostsQuery.cs b/Femto.Modules.Blog/Domain/Posts/Commands/GetAuthorPosts/GetAuthorPostsQuery.cs deleted file mode 100644 index 1334bb8..0000000 --- a/Femto.Modules.Blog/Domain/Posts/Commands/GetAuthorPosts/GetAuthorPostsQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Femto.Modules.Blog.Contracts.Dto; -using MediatR; - -namespace Femto.Modules.Blog.Domain.Posts.Commands.GetAuthorPosts; - -public record GetAuthorPostsQuery(Guid AuthorId, Guid? Cursor, int? Count) : IRequest>; \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetAuthorPosts/GetAuthorPostsQueryHandler.cs b/Femto.Modules.Blog/Domain/Posts/Commands/GetAuthorPosts/GetAuthorPostsQueryHandler.cs deleted file mode 100644 index 47ff33f..0000000 --- a/Femto.Modules.Blog/Domain/Posts/Commands/GetAuthorPosts/GetAuthorPostsQueryHandler.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Dapper; -using Femto.Modules.Blog.Contracts.Dto; -using Femto.Modules.Blog.Infrastructure.DbConnection; -using MediatR; -using Microsoft.Data.SqlClient; - -namespace Femto.Modules.Blog.Domain.Posts.Commands.GetAuthorPosts; - -public class GetAuthorPostsQueryHandler(IDbConnectionFactory connectionFactory) - : IRequestHandler> -{ - public async Task> Handle( - GetAuthorPostsQuery query, - CancellationToken cancellationToken - ) - { - using var conn = connectionFactory.GetConnection(); - - var sql = $$""" - with post_page as ( - select * from blog.post - where blog.post.author_id = @authorId - and (@cursor is null or blog.post.id < @cursor) - order by blog.post.id desc - limit @count - ) - select - p.id as PostId, - p.content as Content, - pm.url as MediaUrl - from post_page p - left join blog.post_media pm on pm.post_id = p.id - order by p.id desc - """; - - var result = await conn.QueryAsync( - sql, - new { authorId = query.AuthorId, cursor = query.Cursor, count = query.Count } - ); - - return result - .GroupBy(row => row.PostId) - .Select(group => new GetAuthorPostsDto( - group.Key, - group.First().Content, - group - .Select(row => row.MediaUrl) - .OfType() - .Select(url => new GetAuthorPostsMediaDto(new Uri(url))) - .ToList() - )) - .ToList(); - } - - internal class QueryResult - { - public Guid PostId { get; set; } - public string Content { get; set; } - public string? MediaUrl { get; set; } - } -} diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/GetPostsQueryResult.cs b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/GetPostsQueryResult.cs new file mode 100644 index 0000000..9ad67e2 --- /dev/null +++ b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/GetPostsQueryResult.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; + +public record GetPostsQueryResult(IList Posts, Guid? Next); \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostAuthorDto.cs b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostAuthorDto.cs new file mode 100644 index 0000000..b057fe8 --- /dev/null +++ b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostAuthorDto.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; + +public record PostAuthorDto(Guid AuthorId, string Username); \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostDto.cs b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostDto.cs new file mode 100644 index 0000000..73ec8f3 --- /dev/null +++ b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostDto.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; + +public record PostDto(Guid PostId, string Text, IList Media, DateTime CreatedAt, PostAuthorDto Author); \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostMediaDto.cs b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostMediaDto.cs new file mode 100644 index 0000000..568cb27 --- /dev/null +++ b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostMediaDto.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; + +public record PostMediaDto(Uri Url); \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQuery.cs b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQuery.cs new file mode 100644 index 0000000..0322b29 --- /dev/null +++ b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQuery.cs @@ -0,0 +1,25 @@ +using Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; +using MediatR; + +namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts; + +public class GetPostsQuery : IRequest +{ + + public string? Username { get; init; } + public Guid? From { get; init; } + public int Amount { get; init; } = 20; + public Guid? AuthorGuid { get; init; } + + /// + /// Default is to load in reverse chronological order + /// TODO this is not exposed on the client as it probably wouldn't work that well + /// + public GetPostsDirection Direction { get; init; } = GetPostsDirection.Backward; +} + +public enum GetPostsDirection +{ + Forward, + Backward +} \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQueryHandler.cs b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQueryHandler.cs new file mode 100644 index 0000000..5b2eb50 --- /dev/null +++ b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQueryHandler.cs @@ -0,0 +1,99 @@ +using Dapper; +using Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; +using Femto.Modules.Blog.Infrastructure.DbConnection; +using MediatR; + +namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts; + +public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory) + : IRequestHandler +{ + public async Task Handle( + GetPostsQuery query, + CancellationToken cancellationToken + ) + { + using var conn = connectionFactory.GetConnection(); + + if (query.Username is not null && query.AuthorGuid is not null) + throw new ArgumentException( + "Cannot specify both username and authorGuid", + nameof(query) + ); + + var orderBy = query.Direction is GetPostsDirection.Backward ? "desc" : "asc"; + var pageFilter = query.Direction is GetPostsDirection.Backward ? "<=" : ">="; + + // lang=sql + var sql = $$""" + with page as ( + select blog.post.*, blog.author.username as Username, blog.author.id as AuthorId + from blog.post + inner join blog.author on blog.author.id = blog.post.author_id + where (@username is null or blog.author.username = @username) + and (@authorGuid is null or blog.author.id = @authorGuid) + and (@cursor is null or blog.post.id {{pageFilter}} @cursor) + order by blog.post.id {{orderBy}} + limit @amount + ) + select + page.id as PostId, + page.content as Content, + blog.post_media.url as MediaUrl, + page.created_on as CreatedAt, + page.Username, + page.AuthorId + from page + left join blog.post_media on blog.post_media.post_id = page.id + order by page.id {{orderBy}} + """; + + var result = await conn.QueryAsync( + sql, + new + { + username = query.Username, + authorGuid = query.AuthorGuid, + cursor = query.From, + // load an extra one to take for the curst + amount = query.Amount + 1, + } + ); + + var rows = result.ToList(); + + var posts = rows.GroupBy(row => row.PostId) + .Select(group => + { + var postId = group.Key; + var post = group.First(); + var media = group + .Select(row => row.MediaUrl) + .OfType() + .Select(url => new PostMediaDto(new Uri(url))) + .ToList(); + return new PostDto( + postId, + post.Content, + media, + post.CreatedAt, + new PostAuthorDto(post.AuthorId, post.Username) + ); + }) + .ToList(); + + var next = rows.Count >= query.Amount ? rows.LastOrDefault()?.PostId : null; + + return new GetPostsQueryResult(posts, next); + } + + internal class QueryResult + { + public Guid PostId { get; set; } + public string Content { get; set; } + public string? MediaUrl { get; set; } + public DateTime CreatedAt { get; set; } + public Guid AuthorId { get; set; } + public string Username { get; set; } + } +}