This commit is contained in:
john 2025-05-03 15:38:57 +02:00
commit ab2e20f7e1
72 changed files with 2000 additions and 0 deletions

View file

@ -0,0 +1,7 @@
namespace Femto.Modules.Blog.Domain.Authors;
internal class Author
{
public string Id { get; private set; } = null!;
public string Name { get; private set; } = null!;
}

View file

@ -0,0 +1,6 @@
using MediatR;
namespace Femto.Modules.Blog.Domain.Posts.Commands.CreatePost;
public record CreatePostCommand(Guid AuthorId, string Content, IEnumerable<Uri> Media)
: IRequest<Guid>;

View file

@ -0,0 +1,21 @@
using Femto.Modules.Blog.Data;
using MediatR;
namespace Femto.Modules.Blog.Domain.Posts.Commands.CreatePost;
internal class CreatePostCommandHandler(BlogContext context)
: IRequestHandler<CreatePostCommand, Guid>
{
public async Task<Guid> Handle(CreatePostCommand request, CancellationToken cancellationToken)
{
var post = new Post(
request.AuthorId,
request.Content,
request.Media.Select((url, idx) => new PostMedia(Guid.CreateVersion7(), url, idx)).ToList()
);
await context.AddAsync(post, cancellationToken);
return post.Id;
}
}

View file

@ -0,0 +1,6 @@
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<IList<GetAuthorPostsDto>>;

View file

@ -0,0 +1,61 @@
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<GetAuthorPostsQuery, IList<GetAuthorPostsDto>>
{
public async Task<IList<GetAuthorPostsDto>> 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<QueryResult>(
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<string>()
.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; }
}
}

View file

@ -0,0 +1,5 @@
using Femto.Common.Domain;
namespace Femto.Modules.Blog.Domain.Posts.Events;
internal record PostCreated(Post Post) : DomainEvent;

View file

@ -0,0 +1,19 @@
using Femto.Modules.Blog.Contracts.Events;
using Femto.Modules.Blog.Domain.Posts.Events;
using Femto.Modules.Blog.Infrastructure.Integration;
using MediatR;
namespace Femto.Modules.Blog.Domain.Posts.Handlers;
internal class PostCreatedHandler(Outbox outbox) : INotificationHandler<PostCreated>
{
public async Task Handle(PostCreated notification, CancellationToken cancellationToken)
{
var post = notification.Post;
await outbox.AddMessage(
post.Id,
new PostCreatedIntegrationEvent(Guid.CreateVersion7(), post.Id, post.Media.Select(m => m.Id)),
cancellationToken
);
}
}

View file

@ -0,0 +1,25 @@
using Femto.Common.Domain;
using Femto.Modules.Blog.Domain.Posts.Events;
using Femto.Modules.Blog.Domain.Posts.Rules;
namespace Femto.Modules.Blog.Domain.Posts;
internal class Post : Entity
{
public Guid Id { get; private set; }
public Guid AuthorId { get; private set; }
public string Content { get; private set; } = null!;
public IList<PostMedia> Media { get; private set; }
private Post() { }
public Post(Guid authorId,string content, IList<PostMedia> media)
{
this.Id = Guid.CreateVersion7();
this.AuthorId = authorId;
this.Content = content;
this.Media = media;
this.AddDomainEvent(new PostCreated(this));
}
}

View file

@ -0,0 +1,17 @@
namespace Femto.Modules.Blog.Domain.Posts;
internal class PostMedia
{
public Guid Id { get; private set; }
public Uri Url { get; private set; }
public int Ordering { get; private set; }
private PostMedia() {}
public PostMedia(Guid id, Uri url, int ordering)
{
this.Id = id;
this.Url = url;
this.Ordering = ordering;
}
}

View file

@ -0,0 +1,9 @@
using Femto.Common.Domain;
namespace Femto.Modules.Blog.Domain.Posts.Rules;
internal class PostMustHaveSomeMediaRule(int mediaCount) : IRule
{
public bool Check() => true || mediaCount > 0;
public string Message => "Post must contain some media";
}