comments
This commit is contained in:
parent
cbf67bf5f1
commit
ce3888f1ab
15 changed files with 162 additions and 21 deletions
3
Femto.Api/Controllers/Posts/Dto/AddPostCommentRequest.cs
Normal file
3
Femto.Api/Controllers/Posts/Dto/AddPostCommentRequest.cs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
namespace Femto.Api.Controllers.Posts.Dto;
|
||||||
|
|
||||||
|
public record AddPostCommentRequest(Guid AuthorId, string Content);
|
3
Femto.Api/Controllers/Posts/Dto/PostCommentDto.cs
Normal file
3
Femto.Api/Controllers/Posts/Dto/PostCommentDto.cs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
namespace Femto.Api.Controllers.Posts.Dto;
|
||||||
|
|
||||||
|
public record PostCommentDto(string Author, string Content, DateTimeOffset PostedOn);
|
|
@ -10,7 +10,8 @@ public record PostDto(
|
||||||
IEnumerable<PostMediaDto> Media,
|
IEnumerable<PostMediaDto> Media,
|
||||||
IEnumerable<PostReactionDto> Reactions,
|
IEnumerable<PostReactionDto> Reactions,
|
||||||
DateTimeOffset CreatedAt,
|
DateTimeOffset CreatedAt,
|
||||||
IEnumerable<string> PossibleReactions
|
IEnumerable<string> PossibleReactions,
|
||||||
|
IEnumerable<PostCommentDto> Comments
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
public static PostDto FromModel(Modules.Blog.Application.Queries.GetPosts.Dto.PostDto post) =>
|
public static PostDto FromModel(Modules.Blog.Application.Queries.GetPosts.Dto.PostDto post) =>
|
||||||
|
@ -21,6 +22,7 @@ public record PostDto(
|
||||||
post.Media.Select(m => new PostMediaDto(m.Url, m.Width, m.Height)),
|
post.Media.Select(m => new PostMediaDto(m.Url, m.Width, m.Height)),
|
||||||
post.Reactions.Select(r => new PostReactionDto(r.Emoji, r.AuthorName, r.ReactedOn)),
|
post.Reactions.Select(r => new PostReactionDto(r.Emoji, r.AuthorName, r.ReactedOn)),
|
||||||
post.CreatedAt,
|
post.CreatedAt,
|
||||||
post.PossibleReactions
|
post.PossibleReactions,
|
||||||
|
post.Comments.Select(c => new PostCommentDto(c.Author, c.Content, c.PostedOn))
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
using Femto.Api.Controllers.Posts.Dto;
|
using Femto.Api.Controllers.Posts.Dto;
|
||||||
using Femto.Common;
|
using Femto.Common;
|
||||||
using Femto.Modules.Blog.Application;
|
using Femto.Modules.Blog.Application;
|
||||||
|
using Femto.Modules.Blog.Application.Commands.AddPostComment;
|
||||||
using Femto.Modules.Blog.Application.Commands.AddPostReaction;
|
using Femto.Modules.Blog.Application.Commands.AddPostReaction;
|
||||||
using Femto.Modules.Blog.Application.Commands.ClearPostReaction;
|
using Femto.Modules.Blog.Application.Commands.ClearPostReaction;
|
||||||
using Femto.Modules.Blog.Application.Commands.CreatePost;
|
using Femto.Modules.Blog.Application.Commands.CreatePost;
|
||||||
|
@ -13,7 +14,7 @@ namespace Femto.Api.Controllers.Posts;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("posts")]
|
[Route("posts")]
|
||||||
public class PostsController(IBlogModule blogModule, ICurrentUserContext currentUserContext)
|
public class PostsController(IBlogModule blogModule, ICurrentUserContext currentUserContext, IAuthorizationService auth)
|
||||||
: ControllerBase
|
: ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
@ -131,4 +132,23 @@ public class PostsController(IBlogModule blogModule, ICurrentUserContext current
|
||||||
|
|
||||||
return this.Ok();
|
return this.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{postId}/comments")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult> AddPostComment(
|
||||||
|
Guid postId,
|
||||||
|
[FromBody] AddPostCommentRequest request,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (currentUserContext.CurrentUser?.Id != request.AuthorId)
|
||||||
|
return this.BadRequest();
|
||||||
|
|
||||||
|
await blogModule.Command(
|
||||||
|
new AddPostCommentCommand(postId, request.AuthorId, request.Content),
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.Ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- Migration: AddCommentToPost
|
||||||
|
-- Created at: 10/08/2025 17:22:42
|
||||||
|
|
||||||
|
CREATE TABLE blog.post_comment
|
||||||
|
(
|
||||||
|
id uuid PRIMARY KEY,
|
||||||
|
post_id uuid REFERENCES blog.post(id),
|
||||||
|
author_id uuid REFERENCES blog.author(id),
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)
|
|
@ -43,6 +43,7 @@ public static class TestDataSeeder
|
||||||
('019691a0-4dd3-7e89-909e-94a6fd19a05e', @id, '["🍆", "🧢", "🧑🏾🎓", "🥕", "🕗"]', 'Some unwitched marbles are thought of simply as currencies. A boundary sees a nepal as a chordal railway.')
|
('019691a0-4dd3-7e89-909e-94a6fd19a05e', @id, '["🍆", "🧢", "🧑🏾🎓", "🥕", "🕗"]', 'Some unwitched marbles are thought of simply as currencies. A boundary sees a nepal as a chordal railway.')
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO blog.post_media
|
INSERT INTO blog.post_media
|
||||||
(id, post_id, url, ordering)
|
(id, post_id, url, ordering)
|
||||||
VALUES
|
VALUES
|
||||||
|
@ -63,6 +64,12 @@ public static class TestDataSeeder
|
||||||
('019691a0-4c3e-726f-b8f6-bcbaabe789ae', @id, '🕗')
|
('019691a0-4c3e-726f-b8f6-bcbaabe789ae', @id, '🕗')
|
||||||
;
|
;
|
||||||
|
|
||||||
|
INSERT INTO blog.post_comment
|
||||||
|
(id, post_id, author_id, content)
|
||||||
|
VALUES
|
||||||
|
('9116da05-49eb-4053-9199-57f54f92e73a', '019691a0-48ed-7eba-b8d3-608e25e07d4b', @id, 'this is a comment!')
|
||||||
|
;
|
||||||
|
|
||||||
INSERT INTO authn.user_identity
|
INSERT INTO authn.user_identity
|
||||||
(id, username, password_hash, password_salt)
|
(id, username, password_hash, password_salt)
|
||||||
VALUES
|
VALUES
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
using Femto.Common.Domain;
|
||||||
|
|
||||||
|
namespace Femto.Modules.Blog.Application.Commands.AddPostComment;
|
||||||
|
|
||||||
|
public record AddPostCommentCommand(Guid PostId, Guid AuthorId, string Content) : ICommand;
|
|
@ -0,0 +1,20 @@
|
||||||
|
using Femto.Common.Domain;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Femto.Modules.Blog.Application.Commands.AddPostComment;
|
||||||
|
|
||||||
|
internal class AddPostCommentCommandHandler(BlogContext context) : ICommandHandler<AddPostCommentCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(AddPostCommentCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var post = await context.Posts.SingleOrDefaultAsync(
|
||||||
|
p => p.Id == request.PostId,
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
if (post is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
post.AddComment(request.AuthorId, request.Content);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,11 +24,9 @@ internal class CreatePostCommandHandler(BlogContext context)
|
||||||
media.Width,
|
media.Width,
|
||||||
media.Height
|
media.Height
|
||||||
))
|
))
|
||||||
.ToList()
|
.ToList(),
|
||||||
)
|
request.IsPublic is true
|
||||||
{
|
);
|
||||||
IsPublic = request.IsPublic is true
|
|
||||||
};
|
|
||||||
|
|
||||||
await context.AddAsync(post, cancellationToken);
|
await context.AddAsync(post, cancellationToken);
|
||||||
|
|
||||||
|
@ -39,7 +37,8 @@ internal class CreatePostCommandHandler(BlogContext context)
|
||||||
post.PostedOn,
|
post.PostedOn,
|
||||||
new PostAuthorDto(post.AuthorId, request.CurrentUser.Username),
|
new PostAuthorDto(post.AuthorId, request.CurrentUser.Username),
|
||||||
[],
|
[],
|
||||||
post.PossibleReactions
|
post.PossibleReactions,
|
||||||
|
[]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@ internal class PostConfiguration : IEntityTypeConfiguration<Post>
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
table.OwnsMany(p => p.Comments).WithOwner();
|
||||||
|
|
||||||
table.Property<string>("PossibleReactionsJson").HasColumnName("possible_reactions");
|
table.Property<string>("PossibleReactionsJson").HasColumnName("possible_reactions");
|
||||||
|
|
||||||
table.Ignore(e => e.PossibleReactions);
|
table.Ignore(e => e.PossibleReactions);
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto;
|
||||||
|
|
||||||
|
public record PostCommentDto(string Author, string Content, DateTimeOffset PostedOn);
|
|
@ -7,5 +7,6 @@ public record PostDto(
|
||||||
DateTimeOffset CreatedAt,
|
DateTimeOffset CreatedAt,
|
||||||
PostAuthorDto Author,
|
PostAuthorDto Author,
|
||||||
IList<PostReactionDto> Reactions,
|
IList<PostReactionDto> Reactions,
|
||||||
IEnumerable<string> PossibleReactions
|
IEnumerable<string> PossibleReactions,
|
||||||
);
|
IList<PostCommentDto> Comments
|
||||||
|
);
|
|
@ -68,7 +68,7 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory)
|
||||||
""",
|
""",
|
||||||
new { postIds }
|
new { postIds }
|
||||||
);
|
);
|
||||||
|
|
||||||
var media = loadMediaResult.ToList();
|
var media = loadMediaResult.ToList();
|
||||||
|
|
||||||
var loadReactionsResult = await conn.QueryAsync<LoadReactionRow>(
|
var loadReactionsResult = await conn.QueryAsync<LoadReactionRow>(
|
||||||
|
@ -87,16 +87,36 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory)
|
||||||
|
|
||||||
var reactions = loadReactionsResult.ToList();
|
var reactions = loadReactionsResult.ToList();
|
||||||
|
|
||||||
|
var loadCommentsResult = await conn.QueryAsync<LoadCommentRow>(
|
||||||
|
"""
|
||||||
|
select
|
||||||
|
pc.id as CommentId,
|
||||||
|
pc.post_id as PostId,
|
||||||
|
pc.content as Content,
|
||||||
|
pc.created_at as PostedOn,
|
||||||
|
a.username as AuthorName
|
||||||
|
from blog.post_comment pc
|
||||||
|
join blog.author a on pc.author_id = a.id
|
||||||
|
where pc.post_id = ANY (@postIds)
|
||||||
|
""",
|
||||||
|
new { postIds }
|
||||||
|
);
|
||||||
|
|
||||||
|
var comments = loadCommentsResult.ToList();
|
||||||
|
|
||||||
return new GetPostsQueryResult(
|
return new GetPostsQueryResult(
|
||||||
posts
|
posts
|
||||||
.Select(p => new PostDto(
|
.Select(p => new PostDto(
|
||||||
p.PostId,
|
p.PostId,
|
||||||
p.Content,
|
p.Content,
|
||||||
media.Where(m => m.PostId == p.PostId).Select(m => new PostMediaDto(
|
media
|
||||||
new Uri(m.MediaUrl),
|
.Where(m => m.PostId == p.PostId)
|
||||||
m.MediaWidth,
|
.Select(m => new PostMediaDto(
|
||||||
m.MediaHeight
|
new Uri(m.MediaUrl),
|
||||||
)).ToList(),
|
m.MediaWidth,
|
||||||
|
m.MediaHeight
|
||||||
|
))
|
||||||
|
.ToList(),
|
||||||
p.PostedOn,
|
p.PostedOn,
|
||||||
new PostAuthorDto(p.AuthorId, p.Username),
|
new PostAuthorDto(p.AuthorId, p.Username),
|
||||||
reactions
|
reactions
|
||||||
|
@ -105,7 +125,11 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory)
|
||||||
.ToList(),
|
.ToList(),
|
||||||
!string.IsNullOrEmpty(p.PossibleReactions)
|
!string.IsNullOrEmpty(p.PossibleReactions)
|
||||||
? JsonSerializer.Deserialize<IEnumerable<string>>(p.PossibleReactions)!
|
? JsonSerializer.Deserialize<IEnumerable<string>>(p.PossibleReactions)!
|
||||||
: []
|
: [],
|
||||||
|
comments
|
||||||
|
.Where(c => c.PostId == p.PostId)
|
||||||
|
.Select(c => new PostCommentDto(c.AuthorName, c.Content, c.PostedOn))
|
||||||
|
.ToList()
|
||||||
))
|
))
|
||||||
.ToList()
|
.ToList()
|
||||||
);
|
);
|
||||||
|
@ -137,4 +161,13 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory)
|
||||||
public string Emoji { get; init; }
|
public string Emoji { get; init; }
|
||||||
public DateTimeOffset CreatedAt { get; init; }
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal record LoadCommentRow
|
||||||
|
{
|
||||||
|
public Guid CommentId { get; init; }
|
||||||
|
public Guid PostId { get; init; }
|
||||||
|
public string Content { get; init; }
|
||||||
|
public DateTimeOffset PostedOn { get; init; }
|
||||||
|
public string AuthorName { get; init; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,9 @@ internal class Post : Entity
|
||||||
public IList<PostMedia> Media { get; private set; }
|
public IList<PostMedia> Media { get; private set; }
|
||||||
|
|
||||||
public IList<PostReaction> Reactions { get; private set; } = [];
|
public IList<PostReaction> Reactions { get; private set; } = [];
|
||||||
public bool IsPublic { get; set; }
|
|
||||||
|
public IList<PostComment> Comments { get; private set; } = [];
|
||||||
|
public bool IsPublic { get; private set; }
|
||||||
|
|
||||||
public DateTimeOffset PostedOn { get; private set; }
|
public DateTimeOffset PostedOn { get; private set; }
|
||||||
|
|
||||||
|
@ -27,7 +29,7 @@ internal class Post : Entity
|
||||||
|
|
||||||
private Post() { }
|
private Post() { }
|
||||||
|
|
||||||
public Post(Guid authorId, string content, IList<PostMedia> media)
|
public Post(Guid authorId, string content, IList<PostMedia> media, bool isPublic)
|
||||||
{
|
{
|
||||||
this.Id = Guid.CreateVersion7();
|
this.Id = Guid.CreateVersion7();
|
||||||
this.AuthorId = authorId;
|
this.AuthorId = authorId;
|
||||||
|
@ -35,6 +37,7 @@ internal class Post : Entity
|
||||||
this.Media = media;
|
this.Media = media;
|
||||||
this.PossibleReactions = AllEmoji.GetRandomEmoji(5);
|
this.PossibleReactions = AllEmoji.GetRandomEmoji(5);
|
||||||
this.PostedOn = DateTimeOffset.UtcNow;
|
this.PostedOn = DateTimeOffset.UtcNow;
|
||||||
|
this.IsPublic = isPublic;
|
||||||
|
|
||||||
this.AddDomainEvent(new PostCreated(this));
|
this.AddDomainEvent(new PostCreated(this));
|
||||||
}
|
}
|
||||||
|
@ -56,4 +59,14 @@ internal class Post : Entity
|
||||||
.Reactions.Where(r => r.AuthorId != reactorId || r.Emoji != emoji)
|
.Reactions.Where(r => r.AuthorId != reactorId || r.Emoji != emoji)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddComment(Guid authorId, string content)
|
||||||
|
{
|
||||||
|
// XXX just ignore empty comments for now. we may want to upgrade this to an error
|
||||||
|
// but it is probably suitable to just consider it a no-op
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.Comments.Add(new PostComment(authorId, content));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
19
Femto.Modules.Blog/Domain/Posts/PostComment.cs
Normal file
19
Femto.Modules.Blog/Domain/Posts/PostComment.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
namespace Femto.Modules.Blog.Domain.Posts;
|
||||||
|
|
||||||
|
internal class PostComment
|
||||||
|
{
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
public Guid AuthorId { get; private set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; private set; }
|
||||||
|
public string Content { get; private set; }
|
||||||
|
|
||||||
|
private PostComment() {}
|
||||||
|
|
||||||
|
public PostComment(Guid authorId, string content)
|
||||||
|
{
|
||||||
|
this.Id = Guid.CreateVersion7();
|
||||||
|
this.AuthorId = authorId;
|
||||||
|
this.Content = content;
|
||||||
|
this.CreatedAt = TimeProvider.System.GetUtcNow();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue