Skip to content

Commit 6987a5b

Browse files
authored
fix: populate topic author in message list (#6)
## Summary <!-- What does this PR change and why? --> ## OpenSpec - [ ] This PR implements an OpenSpec change: `<!-- change name -->` - [ ] No OpenSpec change is needed (small doc/fix, no shipped behavior change). ## Verification - [ ] `pnpm verify` passed. - [ ] A subset was run instead: `<!-- commands -->` because `<!-- reason -->`. ## Impact Checklist - [ ] App code (`apps/`, `packages/`) - [ ] API behavior / OpenAPI contract (`apps/api/src/routes/*.ts` zod-openapi declarations, `api/openapi.json` via `pnpm gen:openapi`) - [ ] Database schema or migration (`packages/db`) - [ ] Deployment (`deployment/`) - [ ] Documentation (`README.md`, `docs/`, `wiki/`) - [ ] CI / scripts (`.github/workflows/`, `scripts/`, `package.json`) - [ ] Secret handling: no real `.env`, tokens, cookies, private keys, database URLs, or user data committed. ## Migration / Deployment Notes <!-- If this PR affects schema, data migration, or production deployment, describe the runbook steps. Otherwise write "None". -->
1 parent 5a6c148 commit 6987a5b

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)