feat(article): add comments on articles - #211
Merged
Merged
Conversation
Stage 5 of the article feature, and the riskiest: comments become polymorphic so an article thread reuses the post comment machinery instead of cloning it. POST /api/v1/articles/:articleId/comments GET /api/v1/articles/:articleId/comments Those are the only two new endpoints. Replies, comment detail, likes, bookmarks and deletion keep working through the existing /comments/:commentId routes, because article comments live in the same table. A separate ArticleComment model would have needed its own like and bookmark tables, two repositories, a mapper and seven duplicated use cases, and every future comment fix applied twice. Comment.postId becomes nullable and articleId appears beside it. Two nullable columns would on their own permit a comment attached to nothing, or to both, so a CHECK constraint holds the invariant. Prisma cannot express one, so it is written by hand and covered by integration tests that fail if a future migrate dev regenerates the table without it. DROP NOT NULL and adding a nullable column are metadata-only in Postgres, so the table is not rewritten. Callers branch on comment.target rather than on which id is null, keeping the two-column representation inside the entity. Two things that had to move in lockstep with the column: - CommentResponse.postId and CommentItemSchema.postId are now nullable. fast-json-stringify coerces a value that does not match its schema instead of rejecting it, so leaving the schema promising a string would have emitted a wrong postId for every article comment rather than failing loudly. - delete-comment decremented Post.commentCount unconditionally. It now branches on the target; posts keep today's behaviour, articles have no counter to maintain. Articles derive commentCount from a relation count. A counter column would drift the way posts.comment_count does, since the reply subtree is removed by a database cascade the application never sees - an e2e test pins that by deleting a parent with two replies and asserting the count falls by three. Writing the e2e suite surfaced a leak in the first version of this change: a stranger commenting on someone else's draft got 409, which confirms the draft exists. Visibility is now checked first, so a stranger gets 404 and only the author is told their own article is not published yet. Notifications: article replies use COMMENT_REPLY, which existed in the Prisma enum but not in the TS one. Post replies keep using COMMENT so their behaviour is unchanged. referenceId is populated for the first time. The existing tests/e2e/comment suite is unchanged and passing, which is the regression gate for the post path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 25, 2026
# [1.5.0](v1.4.0...v1.5.0) (2026-08-25) ### Features * **article:** add comments on articles ([#211](#211)) ([da44a3b](da44a3b))
|
🎉 This PR is included in version 1.5.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Stage 5 of the article feature, and the riskiest one — comments become polymorphic so an article thread reuses the post comment machinery instead of cloning it.
Those are the only two new endpoints. Replies, comment detail, likes, bookmarks and deletion all keep working through the existing
/comments/:commentIdroutes, because article comments live in the same table. That is the entire payoff: a separateArticleCommentmodel would have needed its own like and bookmark tables, two repositories, a mapper, seven duplicated use cases and six more routes — and every future comment fix applied twice.The schema change
Comment.postIdbecomes nullable andarticleIdappears beside it. Two nullable columns would on their own permit a comment attached to nothing, or to both, so the invariant is held by a CHECK constraint:Prisma cannot express a CHECK, which also means it does not know this one exists — so
tests/integration/persistence/comment-target-constraint.test.tsis the guard. It asserts both-null and both-set are rejected on insert and on update, so a futuremigrate devthat regenerates the table without the constraint fails loudly instead of the invariant quietly disappearing.DROP NOT NULLand adding a nullable column are metadata-only in Postgres, so no table rewrite happens. Callers branch oncomment.targetrather than on which id is null, keeping the two-column representation inside the entity.Two things that had to move in lockstep with the column
1. The response schema.
CommentResponse.postIdandCommentItemSchema.postIdare now nullable, witharticleIdalongside. fast-json-stringify coerces a value that does not match its schema rather than rejecting it, so leaving the schema promising astringwould have emitted a wrongpostIdfor every article comment — silent data corruption, not an error.2.
delete-comment. It decrementedPost.commentCountunconditionally. It now branches on the target: posts keep today's behaviour, articles have no counter to maintain.Article comment count is derived
_count: { select: { comments: true } }, not a column. A counter would drift exactly the wayposts.comment_countdoes, because the reply subtree is removed by a database cascade the application never observes. An e2e test pins this: a parent with two replies is deleted, and the count falls by three.Writing the e2e suite found a leak in my first version
A stranger commenting on someone else's draft got 409 ArticleNotPublishedError — which confirms the draft exists. Visibility is now checked before publication status:
The same split is covered for the comment list.
Notifications
Article replies use
COMMENT_REPLY, which already existed in the Prisma enum but not the TypeScript one. Post replies keep usingCOMMENT, so their behaviour is unchanged.referenceIdis populated for the first time — it has been plumbed end to end since the beginning and never set.Verification
740 unit tests pass (721 existing + 19 new),
lint,format:check,buildclean. Booting the app confirms DI still resolves (GetPostCommentsUseCasegained a repository) and that the shared/comments/:commentIdroutes are intact.The regression gate:
tests/e2e/comment/*is untouched by this PR and must stay green — that is what proves the post comment path still behaves exactly as before. The unit test for the post half ofCreateCommentUseCaseis likewise left in place; only its input shape changed.New e2e coverage lives in
tests/e2e/article/comments.test.ts: creation, nested replies through/comments/:id/replies, reading and liking and deleting through the shared routes, the draft splits above, cross-thread parent rejection, and the derived comment count.Note
GetPostCommentsUseCasenow serves both targets, so its name is misleading. Renaming it touches the DI key and the controller, so per the plan it happens in stage 6 alongside deleting the deprecatedfindTopLevelByPostIdwrapper, rather than adding diff noise to the migration PR.Type of Change
Checklist
feature/,fix/,chore/,docs/)🤖 Generated with Claude Code