From 6f6c68ce568351c189c1394cfeea73c1f2d6f105 Mon Sep 17 00:00:00 2001 From: Guido X Jansen Date: Mon, 2 Mar 2026 23:47:43 +0100 Subject: [PATCH] feat(replies): include isModDeleted in serialized response and show tombstones (#114) Add isAuthorDeleted and isModDeleted to the reply JSON schema so Fastify includes them in responses. Remove the query filter that excluded mod-deleted replies, allowing the frontend to render tombstone placeholders. Fixes barazo-forum/barazo-workspace#62 --- src/routes/replies.ts | 4 ++- tests/unit/routes/replies.test.ts | 48 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/routes/replies.ts b/src/routes/replies.ts index 7593d2d..5d474e6 100644 --- a/src/routes/replies.ts +++ b/src/routes/replies.ts @@ -80,6 +80,8 @@ const replyJsonSchema = { cid: { type: 'string' as const }, depth: { type: 'integer' as const }, reactionCount: { type: 'integer' as const }, + isAuthorDeleted: { type: 'boolean' as const }, + isModDeleted: { type: 'boolean' as const }, isMuted: { type: 'boolean' as const }, isMutedWord: { type: 'boolean' as const }, ozoneLabel: { type: ['string', 'null'] as const }, @@ -125,6 +127,7 @@ function serializeReply(row: typeof replies.$inferSelect) { depth, reactionCount: row.reactionCount, isAuthorDeleted: row.isAuthorDeleted, + isModDeleted: row.isModDeleted, createdAt: row.createdAt.toISOString(), indexedAt: row.indexedAt.toISOString(), } @@ -589,7 +592,6 @@ export function replyRoutes(): FastifyPluginCallback { const conditions = [ eq(replies.rootUri, decodedTopicUri), eq(replies.moderationStatus, 'approved'), - eq(replies.isModDeleted, false), ] // Exclude replies by blocked authors diff --git a/tests/unit/routes/replies.test.ts b/tests/unit/routes/replies.test.ts index 23e1a61..2f17709 100644 --- a/tests/unit/routes/replies.test.ts +++ b/tests/unit/routes/replies.test.ts @@ -2213,6 +2213,54 @@ describe('reply routes', () => { expect(body.replies).toHaveLength(1) expect(body.replies[0]?.content).toBe('[Removed by moderator]') }) + + it('includes isModDeleted in serialized response', async () => { + selectChain.where.mockResolvedValueOnce([sampleTopicRow()]) + + const modDeletedReply = sampleReplyRow({ + isModDeleted: true, + isAuthorDeleted: false, + }) + selectChain.limit.mockResolvedValueOnce([modDeletedReply]) + + const encodedTopicUri = encodeURIComponent(TEST_TOPIC_URI) + const response = await app.inject({ + method: 'GET', + url: `/api/topics/${encodedTopicUri}/replies`, + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ + replies: Array<{ isAuthorDeleted: boolean; isModDeleted: boolean }> + }>() + expect(body.replies).toHaveLength(1) + expect(body.replies[0]?.isModDeleted).toBe(true) + expect(body.replies[0]?.isAuthorDeleted).toBe(false) + }) + + it('includes isAuthorDeleted and isModDeleted as false for normal replies', async () => { + selectChain.where.mockResolvedValueOnce([sampleTopicRow()]) + + const normalReply = sampleReplyRow({ + isAuthorDeleted: false, + isModDeleted: false, + }) + selectChain.limit.mockResolvedValueOnce([normalReply]) + + const encodedTopicUri = encodeURIComponent(TEST_TOPIC_URI) + const response = await app.inject({ + method: 'GET', + url: `/api/topics/${encodedTopicUri}/replies`, + }) + + expect(response.statusCode).toBe(200) + const body = response.json<{ + replies: Array<{ isAuthorDeleted: boolean; isModDeleted: boolean }> + }>() + expect(body.replies).toHaveLength(1) + expect(body.replies[0]?.isModDeleted).toBe(false) + expect(body.replies[0]?.isAuthorDeleted).toBe(false) + }) }) describe('PUT /api/replies/:uri (error branches)', () => { -- 2.51.2