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; } } }