diff --git a/Directory.Build.props b/Directory.Build.props
index e16080e..0751e1d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- 0.1.30
+ 0.1.31
diff --git a/Femto.Api/Controllers/Posts/Dto/AddPostCommentRequest.cs b/Femto.Api/Controllers/Posts/Dto/AddPostCommentRequest.cs
new file mode 100644
index 0000000..7546af0
--- /dev/null
+++ b/Femto.Api/Controllers/Posts/Dto/AddPostCommentRequest.cs
@@ -0,0 +1,3 @@
+namespace Femto.Api.Controllers.Posts.Dto;
+
+public record AddPostCommentRequest(Guid AuthorId, string Content);
\ No newline at end of file
diff --git a/Femto.Api/Controllers/Posts/Dto/PostCommentDto.cs b/Femto.Api/Controllers/Posts/Dto/PostCommentDto.cs
new file mode 100644
index 0000000..04e180a
--- /dev/null
+++ b/Femto.Api/Controllers/Posts/Dto/PostCommentDto.cs
@@ -0,0 +1,3 @@
+namespace Femto.Api.Controllers.Posts.Dto;
+
+public record PostCommentDto(string Author, string Content, DateTimeOffset PostedOn);
\ No newline at end of file
diff --git a/Femto.Api/Controllers/Posts/Dto/PostDto.cs b/Femto.Api/Controllers/Posts/Dto/PostDto.cs
index 2e6e827..c9af7c6 100644
--- a/Femto.Api/Controllers/Posts/Dto/PostDto.cs
+++ b/Femto.Api/Controllers/Posts/Dto/PostDto.cs
@@ -10,7 +10,8 @@ public record PostDto(
IEnumerable Media,
IEnumerable Reactions,
DateTimeOffset CreatedAt,
- IEnumerable PossibleReactions
+ IEnumerable PossibleReactions,
+ IEnumerable Comments
)
{
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.Reactions.Select(r => new PostReactionDto(r.Emoji, r.AuthorName, r.ReactedOn)),
post.CreatedAt,
- post.PossibleReactions
+ post.PossibleReactions,
+ post.Comments.Select(c => new PostCommentDto(c.Author, c.Content, c.PostedOn))
);
-}
+}
\ No newline at end of file
diff --git a/Femto.Api/Controllers/Posts/PostsController.cs b/Femto.Api/Controllers/Posts/PostsController.cs
index bb4bca7..ed882f7 100644
--- a/Femto.Api/Controllers/Posts/PostsController.cs
+++ b/Femto.Api/Controllers/Posts/PostsController.cs
@@ -1,6 +1,7 @@
using Femto.Api.Controllers.Posts.Dto;
using Femto.Common;
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.ClearPostReaction;
using Femto.Modules.Blog.Application.Commands.CreatePost;
@@ -13,7 +14,7 @@ namespace Femto.Api.Controllers.Posts;
[ApiController]
[Route("posts")]
-public class PostsController(IBlogModule blogModule, ICurrentUserContext currentUserContext)
+public class PostsController(IBlogModule blogModule, ICurrentUserContext currentUserContext, IAuthorizationService auth)
: ControllerBase
{
[HttpGet]
@@ -131,4 +132,23 @@ public class PostsController(IBlogModule blogModule, ICurrentUserContext current
return this.Ok();
}
+
+ [HttpPost("{postId}/comments")]
+ [Authorize]
+ public async Task 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();
+ }
}
diff --git a/Femto.Database/Migrations/20250810172242_AddCommentToPost.sql b/Femto.Database/Migrations/20250810172242_AddCommentToPost.sql
new file mode 100644
index 0000000..44e0086
--- /dev/null
+++ b/Femto.Database/Migrations/20250810172242_AddCommentToPost.sql
@@ -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()
+)
\ No newline at end of file
diff --git a/Femto.Database/Seed/TestDataSeeder.cs b/Femto.Database/Seed/TestDataSeeder.cs
index 433f73c..2c8efcc 100644
--- a/Femto.Database/Seed/TestDataSeeder.cs
+++ b/Femto.Database/Seed/TestDataSeeder.cs
@@ -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.')
;
+
INSERT INTO blog.post_media
(id, post_id, url, ordering)
VALUES
@@ -63,6 +64,12 @@ public static class TestDataSeeder
('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
(id, username, password_hash, password_salt)
VALUES
diff --git a/Femto.Modules.Blog.Data/Class1.cs b/Femto.Modules.Blog.Data/Class1.cs
deleted file mode 100644
index 3be8b2a..0000000
--- a/Femto.Modules.Blog.Data/Class1.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Femto.Modules.Blog.Data;
-
-public class Class1
-{
-}
\ No newline at end of file
diff --git a/Femto.Modules.Blog.Data/Femto.Modules.Blog.Data.csproj b/Femto.Modules.Blog.Data/Femto.Modules.Blog.Data.csproj
deleted file mode 100644
index 17b910f..0000000
--- a/Femto.Modules.Blog.Data/Femto.Modules.Blog.Data.csproj
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
- net9.0
- enable
- enable
-
-
-
diff --git a/Femto.Modules.Blog.Domain/Femto.Modules.Blog.Domain.csproj b/Femto.Modules.Blog.Domain/Femto.Modules.Blog.Domain.csproj
deleted file mode 100644
index 6ae6742..0000000
--- a/Femto.Modules.Blog.Domain/Femto.Modules.Blog.Domain.csproj
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- net9.0
- enable
- enable
-
-
-
-
-
-
-
-
-
-
-
-
-
- ..\..\..\..\.nuget\packages\microsoft.entityframeworkcore\9.0.4\lib\net8.0\Microsoft.EntityFrameworkCore.dll
-
-
-
-
diff --git a/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommand.cs b/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommand.cs
new file mode 100644
index 0000000..445c59e
--- /dev/null
+++ b/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommand.cs
@@ -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;
\ No newline at end of file
diff --git a/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommandHandler.cs b/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommandHandler.cs
new file mode 100644
index 0000000..6e52877
--- /dev/null
+++ b/Femto.Modules.Blog/Application/Commands/AddPostComment/AddPostCommentCommandHandler.cs
@@ -0,0 +1,20 @@
+using Femto.Common.Domain;
+using Microsoft.EntityFrameworkCore;
+
+namespace Femto.Modules.Blog.Application.Commands.AddPostComment;
+
+internal class AddPostCommentCommandHandler(BlogContext context) : ICommandHandler
+{
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs
index 2d9c713..25bab45 100644
--- a/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs
+++ b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs
@@ -24,11 +24,9 @@ internal class CreatePostCommandHandler(BlogContext context)
media.Width,
media.Height
))
- .ToList()
- )
- {
- IsPublic = request.IsPublic is true
- };
+ .ToList(),
+ request.IsPublic is true
+ );
await context.AddAsync(post, cancellationToken);
@@ -39,7 +37,8 @@ internal class CreatePostCommandHandler(BlogContext context)
post.PostedOn,
new PostAuthorDto(post.AuthorId, request.CurrentUser.Username),
[],
- post.PossibleReactions
+ post.PossibleReactions,
+ []
);
}
}
diff --git a/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs b/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs
index b1defec..630cbe2 100644
--- a/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs
+++ b/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs
@@ -24,6 +24,8 @@ internal class PostConfiguration : IEntityTypeConfiguration
}
);
+ table.OwnsMany(p => p.Comments).WithOwner();
+
table.Property("PossibleReactionsJson").HasColumnName("possible_reactions");
table.Ignore(e => e.PossibleReactions);
diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostCommentDto.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostCommentDto.cs
new file mode 100644
index 0000000..55ea5e8
--- /dev/null
+++ b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostCommentDto.cs
@@ -0,0 +1,3 @@
+namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto;
+
+public record PostCommentDto(string Author, string Content, DateTimeOffset PostedOn);
\ No newline at end of file
diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs
index b8b6a3d..63efede 100644
--- a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs
+++ b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs
@@ -7,5 +7,6 @@ public record PostDto(
DateTimeOffset CreatedAt,
PostAuthorDto Author,
IList Reactions,
- IEnumerable PossibleReactions
-);
+ IEnumerable PossibleReactions,
+ IList Comments
+);
\ No newline at end of file
diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs
index 0af48ee..c57627f 100644
--- a/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs
+++ b/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs
@@ -68,7 +68,7 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory)
""",
new { postIds }
);
-
+
var media = loadMediaResult.ToList();
var loadReactionsResult = await conn.QueryAsync(
@@ -87,16 +87,36 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory)
var reactions = loadReactionsResult.ToList();
+ var loadCommentsResult = await conn.QueryAsync(
+ """
+ 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(
posts
.Select(p => new PostDto(
p.PostId,
p.Content,
- media.Where(m => m.PostId == p.PostId).Select(m => new PostMediaDto(
- new Uri(m.MediaUrl),
- m.MediaWidth,
- m.MediaHeight
- )).ToList(),
+ media
+ .Where(m => m.PostId == p.PostId)
+ .Select(m => new PostMediaDto(
+ new Uri(m.MediaUrl),
+ m.MediaWidth,
+ m.MediaHeight
+ ))
+ .ToList(),
p.PostedOn,
new PostAuthorDto(p.AuthorId, p.Username),
reactions
@@ -105,7 +125,11 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory)
.ToList(),
!string.IsNullOrEmpty(p.PossibleReactions)
? JsonSerializer.Deserialize>(p.PossibleReactions)!
- : []
+ : [],
+ comments
+ .Where(c => c.PostId == p.PostId)
+ .Select(c => new PostCommentDto(c.AuthorName, c.Content, c.PostedOn))
+ .ToList()
))
.ToList()
);
@@ -137,4 +161,13 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory)
public string Emoji { 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; }
+ }
}
diff --git a/Femto.Modules.Blog/Domain/Posts/Post.cs b/Femto.Modules.Blog/Domain/Posts/Post.cs
index dc4d937..b5a9b2d 100644
--- a/Femto.Modules.Blog/Domain/Posts/Post.cs
+++ b/Femto.Modules.Blog/Domain/Posts/Post.cs
@@ -13,7 +13,9 @@ internal class Post : Entity
public IList Media { get; private set; }
public IList Reactions { get; private set; } = [];
- public bool IsPublic { get; set; }
+
+ public IList Comments { get; private set; } = [];
+ public bool IsPublic { get; private set; }
public DateTimeOffset PostedOn { get; private set; }
@@ -27,7 +29,7 @@ internal class Post : Entity
private Post() { }
- public Post(Guid authorId, string content, IList media)
+ public Post(Guid authorId, string content, IList media, bool isPublic)
{
this.Id = Guid.CreateVersion7();
this.AuthorId = authorId;
@@ -35,6 +37,7 @@ internal class Post : Entity
this.Media = media;
this.PossibleReactions = AllEmoji.GetRandomEmoji(5);
this.PostedOn = DateTimeOffset.UtcNow;
+ this.IsPublic = isPublic;
this.AddDomainEvent(new PostCreated(this));
}
@@ -56,4 +59,14 @@ internal class Post : Entity
.Reactions.Where(r => r.AuthorId != reactorId || r.Emoji != emoji)
.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));
+ }
}
diff --git a/Femto.Modules.Blog/Domain/Posts/PostComment.cs b/Femto.Modules.Blog/Domain/Posts/PostComment.cs
new file mode 100644
index 0000000..6f658a8
--- /dev/null
+++ b/Femto.Modules.Blog/Domain/Posts/PostComment.cs
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/Femto.Modules.Files/Domain/Files/File.cs b/Femto.Modules.Files/Domain/Files/File.cs
deleted file mode 100644
index 9600ceb..0000000
--- a/Femto.Modules.Files/Domain/Files/File.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Femto.Modules.Files.Domain.Files;
-
-public class File
-{
- Guid Id { get; set; }
-
-
-}
\ No newline at end of file
diff --git a/Femto.Modules.Files/Femto.Modules.Files.csproj b/Femto.Modules.Files/Femto.Modules.Files.csproj
deleted file mode 100644
index 17b910f..0000000
--- a/Femto.Modules.Files/Femto.Modules.Files.csproj
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
- net9.0
- enable
- enable
-
-
-