Skip to content

Commit f5c0a76

Browse files
committed
fix: populate topic author in message list
GET /messages always returned the empty placeholder for topic.author because getMessageRelations handed back a raw topic row that has no author relation; the messageDTOSchema contract declares a real author and the route test mocked one, hiding the gap. Fetch the topic author in getMessageRelations and attach it, so the web message list can show who owns the topic. Covered by the new message-relations test, which fails against the previous implementation.
1 parent a3e2b0a commit f5c0a76

3 files changed

Lines changed: 73 additions & 2 deletions

File tree

‎apps/api/src/lib/message.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,12 @@ export async function getMessageRelations(msg: typeof messages.$inferSelect) {
100100
msg.topicId ? topicQueries.getById(msg.topicId) : null,
101101
msg.replyId ? replyQueries.getById(msg.replyId) : null,
102102
]);
103+
const topicAuthor = topic ? await userQueries.getById(topic.authorId) : null;
104+
103105
return {
104106
...msg,
105107
author,
106-
topic,
108+
topic: topic ? { ...topic, author: topicAuthor } : null,
107109
reply,
108110
isInvalid: !author || !topic,
109111
};

‎apps/api/src/routes/message.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ message.openapi(listMessagesRoute, async (c) => {
7575
author: { loginname: author.loginname, avatar_url: author.avatar ?? "" },
7676
topic: {
7777
id: String(topic.id),
78-
author: { loginname: "", avatar_url: "" },
78+
author: topic.author
79+
? { loginname: topic.author.loginname, avatar_url: topic.author.avatar ?? "" }
80+
: { loginname: "", avatar_url: "" },
7981
title: topic.title ?? "",
8082
last_reply_at: topic.lastReplyAt ? topic.lastReplyAt.toISOString() : null,
8183
},
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
2+
3+
const mocks = vi.hoisted(() => ({
4+
userGetById: vi.fn(),
5+
topicGetById: vi.fn(),
6+
replyGetById: vi.fn(),
7+
}));
8+
9+
vi.mock("../src/lib/db", () => ({
10+
getDb: vi.fn(),
11+
userQueries: { getById: mocks.userGetById },
12+
topicQueries: { getById: mocks.topicGetById },
13+
replyQueries: { getById: mocks.replyGetById },
14+
}));
15+
16+
vi.mock("../src/lib/mail", () => ({
17+
sendAtNotifyMail: vi.fn(),
18+
sendReplyNotifyMail: vi.fn(),
19+
}));
20+
21+
import { getMessageRelations } from "../src/lib/message";
22+
23+
const messageRow = {
24+
id: 1,
25+
type: "reply",
26+
masterId: 10,
27+
authorId: 20,
28+
topicId: 2,
29+
replyId: null,
30+
hasRead: false,
31+
createAt: new Date("2026-08-05T00:00:00.000Z"),
32+
};
33+
34+
const actor = { id: 20, loginname: "alice", avatar: "https://example.com/alice.png" };
35+
const topicAuthor = { id: 30, loginname: "bob", avatar: "https://example.com/bob.png" };
36+
const topic = { id: 2, title: "话题标题", authorId: 30, lastReplyAt: null };
37+
38+
describe("getMessageRelations", () => {
39+
beforeEach(() => {
40+
mocks.userGetById.mockReset();
41+
mocks.topicGetById.mockReset();
42+
mocks.replyGetById.mockReset();
43+
mocks.userGetById.mockImplementation(async (id: number) => {
44+
if (id === actor.id) return actor;
45+
if (id === topicAuthor.id) return topicAuthor;
46+
return null;
47+
});
48+
mocks.topicGetById.mockResolvedValue(topic);
49+
mocks.replyGetById.mockResolvedValue(null);
50+
});
51+
52+
it("attaches the topic author so /messages can render it", async () => {
53+
const relations = await getMessageRelations(messageRow);
54+
55+
expect(relations.author).toEqual(actor);
56+
expect(relations.topic?.author).toEqual(topicAuthor);
57+
});
58+
59+
it("keeps topic null and flags invalid when the topic is missing", async () => {
60+
mocks.topicGetById.mockResolvedValue(null);
61+
62+
const relations = await getMessageRelations({ ...messageRow, topicId: null });
63+
64+
expect(relations.topic).toBeNull();
65+
expect(relations.isInvalid).toBe(true);
66+
});
67+
});

0 commit comments

Comments
 (0)