Barazo AppView backend barazo.forum

fix(topics): enrich author profile in single-topic detail endpoints (#138)

The GET /api/topics/by-rkey/:rkey and GET /api/topics/:uri endpoints
returned raw authorDid without resolving the author profile, while the
list endpoint already called resolveAuthors(). This caused the frontend
to display did:plc:... instead of the user's display name and avatar.

authored by

Guido X Jansen and committed by
GitHub
a3bb0186 bf6ea21b

+107 -2
+24 -2
src/routes/topics.ts
··· 831 throw forbidden('Content restricted by maturity settings') 832 } 833 834 - return reply.status(200).send(serializeTopic(row, categoryRating)) 835 } 836 ) 837 ··· 907 throw forbidden('Content restricted by maturity settings') 908 } 909 910 - return reply.status(200).send(serializeTopic(row, categoryRating)) 911 } 912 ) 913
··· 831 throw forbidden('Content restricted by maturity settings') 832 } 833 834 + const serialized = serializeTopic(row, categoryRating) 835 + const authorMap = await resolveAuthors([row.authorDid], communityDid, db) 836 + 837 + return reply.status(200).send({ 838 + ...serialized, 839 + author: authorMap.get(row.authorDid) ?? { 840 + did: row.authorDid, 841 + handle: row.authorDid, 842 + displayName: null, 843 + avatarUrl: null, 844 + }, 845 + }) 846 } 847 ) 848 ··· 918 throw forbidden('Content restricted by maturity settings') 919 } 920 921 + const serialized = serializeTopic(row, categoryRating) 922 + const authorMap = await resolveAuthors([row.authorDid], communityDid, db) 923 + 924 + return reply.status(200).send({ 925 + ...serialized, 926 + author: authorMap.get(row.authorDid) ?? { 927 + did: row.authorDid, 928 + handle: row.authorDid, 929 + displayName: null, 930 + avatarUrl: null, 931 + }, 932 + }) 933 } 934 ) 935
+83
tests/unit/routes/topics.test.ts
··· 949 expect(body.title).toBe('Test Topic Title') 950 }) 951 952 it('returns 404 for non-existent topic', async () => { 953 selectChain.where.mockResolvedValueOnce([]) 954 ··· 1047 const body = response.json<{ uri: string; title: string; rkey: string }>() 1048 expect(body.uri).toBe(TEST_URI) 1049 expect(body.title).toBe('Test Topic Title') 1050 }) 1051 1052 it('returns 404 for non-existent rkey', async () => {
··· 949 expect(body.title).toBe('Test Topic Title') 950 }) 951 952 + it('enriches author profile in single topic response', async () => { 953 + const row = sampleTopicRow() 954 + // 1. find topic 955 + selectChain.where.mockResolvedValueOnce([row]) 956 + // 2. category maturity rating 957 + selectChain.where.mockResolvedValueOnce([{ maturityRating: 'safe' }]) 958 + // 3. user profile (maturity) 959 + selectChain.where.mockResolvedValueOnce([{ declaredAge: null, maturityPref: 'safe' }]) 960 + // 4. age threshold 961 + selectChain.where.mockResolvedValueOnce([{ ageThreshold: 16 }]) 962 + // 5. resolveAuthors: users table 963 + selectChain.where.mockResolvedValueOnce([ 964 + { 965 + did: TEST_DID, 966 + handle: TEST_HANDLE, 967 + displayName: 'Jay', 968 + avatarUrl: 'https://cdn.example.com/jay.jpg', 969 + bannerUrl: null, 970 + bio: null, 971 + }, 972 + ]) 973 + // 6. resolveAuthors: community profiles 974 + selectChain.where.mockResolvedValueOnce([]) 975 + 976 + const encodedUri = encodeURIComponent(TEST_URI) 977 + const response = await app.inject({ 978 + method: 'GET', 979 + url: `/api/topics/${encodedUri}`, 980 + }) 981 + 982 + expect(response.statusCode).toBe(200) 983 + const body = response.json<{ 984 + author: { did: string; handle: string; displayName: string; avatarUrl: string } 985 + }>() 986 + expect(body.author).toEqual({ 987 + did: TEST_DID, 988 + handle: TEST_HANDLE, 989 + displayName: 'Jay', 990 + avatarUrl: 'https://cdn.example.com/jay.jpg', 991 + }) 992 + }) 993 + 994 it('returns 404 for non-existent topic', async () => { 995 selectChain.where.mockResolvedValueOnce([]) 996 ··· 1089 const body = response.json<{ uri: string; title: string; rkey: string }>() 1090 expect(body.uri).toBe(TEST_URI) 1091 expect(body.title).toBe('Test Topic Title') 1092 + }) 1093 + 1094 + it('enriches author profile in by-rkey response', async () => { 1095 + const row = sampleTopicRow() 1096 + // 1. find topic 1097 + selectChain.where.mockResolvedValueOnce([row]) 1098 + // 2. category maturity rating 1099 + selectChain.where.mockResolvedValueOnce([{ maturityRating: 'safe' }]) 1100 + // 3. user profile (maturity) 1101 + selectChain.where.mockResolvedValueOnce([{ declaredAge: null, maturityPref: 'safe' }]) 1102 + // 4. age threshold 1103 + selectChain.where.mockResolvedValueOnce([{ ageThreshold: 16 }]) 1104 + // 5. resolveAuthors: users table 1105 + selectChain.where.mockResolvedValueOnce([ 1106 + { 1107 + did: TEST_DID, 1108 + handle: TEST_HANDLE, 1109 + displayName: 'Jay', 1110 + avatarUrl: 'https://cdn.example.com/jay.jpg', 1111 + bannerUrl: null, 1112 + bio: null, 1113 + }, 1114 + ]) 1115 + // 6. resolveAuthors: community profiles 1116 + selectChain.where.mockResolvedValueOnce([]) 1117 + 1118 + const response = await app.inject({ 1119 + method: 'GET', 1120 + url: '/api/topics/by-rkey/abc123', 1121 + }) 1122 + 1123 + expect(response.statusCode).toBe(200) 1124 + const body = response.json<{ 1125 + author: { did: string; handle: string; displayName: string; avatarUrl: string } 1126 + }>() 1127 + expect(body.author).toEqual({ 1128 + did: TEST_DID, 1129 + handle: TEST_HANDLE, 1130 + displayName: 'Jay', 1131 + avatarUrl: 'https://cdn.example.com/jay.jpg', 1132 + }) 1133 }) 1134 1135 it('returns 404 for non-existent rkey', async () => {