Testing implementation for private data in ATProto with ATPKeyserver and ATCute tools
at main 171 lines 5.3 kB view raw
1import { db } from '@api/db/db' 2import type { Did, ResourceUri } from '@atcute/lexicons' 3import type { AppWafrnContentDefs } from '@watproto/lexicon' 4 5type FeedParams = { 6 did: Did 7 limit: number 8} 9 10type FollowingFeedParams = { 11 viewer: Did 12 limit: number 13} 14 15export async function getActorFeed({ did, limit }: FeedParams) { 16 const [publicPostRows, privatePostRows] = await Promise.all([ 17 db 18 .selectFrom('public_posts') 19 .selectAll() 20 .where('author_did', '=', did) 21 .orderBy('created_at', 'desc') 22 .limit(limit * 2) 23 .execute(), 24 db 25 .selectFrom('private_posts') 26 .selectAll() 27 .where('author_did', '=', did) 28 .orderBy('created_at', 'desc') 29 .limit(limit * 2) 30 .execute() 31 ]) 32 33 // Fetch tags for all public posts in one query 34 const publicPostUris = publicPostRows.map((p) => p.uri) 35 const tagRows = 36 publicPostUris.length > 0 37 ? await db 38 .selectFrom('public_post_tags') 39 .selectAll() 40 .where('post_uri', 'in', publicPostUris) 41 .execute() 42 : [] 43 44 // Group tags by post URI 45 const tagsByPostUri = new Map<string, string[]>() 46 for (const tagRow of tagRows) { 47 const existing = tagsByPostUri.get(tagRow.post_uri) ?? [] 48 existing.push(tagRow.tag_name) 49 tagsByPostUri.set(tagRow.post_uri, existing) 50 } 51 52 const publicPosts = publicPostRows.map( 53 (p) => 54 ({ 55 $type: 'app.wafrn.content.defs#publicPostView', 56 uri: p.uri as ResourceUri, 57 content: { 58 contentHTML: p.content_html, 59 contentMarkdown: p.content_markdown, 60 contentWarning: p.content_warning ?? '', 61 tags: tagsByPostUri.get(p.uri) ?? [] 62 }, 63 createdAt: new Date(p.created_at).toISOString(), 64 updatedAt: new Date(p.updated_at).toISOString() 65 }) satisfies AppWafrnContentDefs.PublicPostView 66 ) 67 const privatePosts = privatePostRows.map( 68 (p) => 69 ({ 70 $type: 'app.wafrn.content.defs#privatePostView', 71 uri: p.uri as ResourceUri, 72 visibility: p.visibility, 73 keyVersion: p.key_version, 74 encryptedContent: p.encrypted_content, 75 createdAt: new Date(p.created_at).toISOString(), 76 updatedAt: new Date(p.updated_at).toISOString() 77 }) satisfies AppWafrnContentDefs.PrivatePostView 78 ) 79 80 const posts = [...publicPosts, ...privatePosts].sort((a, b) => { 81 return Number(a.createdAt < b.createdAt) 82 }) 83 84 return posts 85} 86 87/** 88 * Get feed of posts from accounts the viewer follows. 89 * Uses efficient JOINs following Kysely best practices. 90 */ 91export async function getFollowingFeed({ 92 viewer, 93 limit 94}: FollowingFeedParams) { 95 // Step 1: Fetch posts from followed accounts using JOINs (efficient!) 96 // Use 2x limit to account for mixed public/private posts 97 const [publicPostRows, privatePostRows] = await Promise.all([ 98 db 99 .selectFrom('public_posts') 100 .innerJoin('follows', 'public_posts.author_did', 'follows.followee_did') 101 .selectAll('public_posts') 102 .where('follows.follower_did', '=', viewer) 103 .orderBy('public_posts.created_at', 'desc') 104 .limit(limit * 2) 105 .execute(), 106 db 107 .selectFrom('private_posts') 108 .innerJoin('follows', 'private_posts.author_did', 'follows.followee_did') 109 .selectAll('private_posts') 110 .where('follows.follower_did', '=', viewer) 111 .orderBy('private_posts.created_at', 'desc') 112 .limit(limit * 2) 113 .execute() 114 ]) 115 116 // Step 2: Batch fetch tags for all public posts (avoid N+1 queries) 117 const publicPostUris = publicPostRows.map((p) => p.uri) 118 const tagRows = 119 publicPostUris.length > 0 120 ? await db 121 .selectFrom('public_post_tags') 122 .selectAll() 123 .where('post_uri', 'in', publicPostUris) 124 .execute() 125 : [] 126 127 // Step 3: Group tags by post URI for efficient lookup 128 const tagsByPostUri = new Map<string, string[]>() 129 for (const tagRow of tagRows) { 130 const existing = tagsByPostUri.get(tagRow.post_uri) ?? [] 131 existing.push(tagRow.tag_name) 132 tagsByPostUri.set(tagRow.post_uri, existing) 133 } 134 135 // Step 4: Transform database rows to API response format 136 const publicPosts = publicPostRows.map( 137 (p) => 138 ({ 139 $type: 'app.wafrn.content.defs#publicPostView', 140 uri: p.uri as ResourceUri, 141 content: { 142 contentHTML: p.content_html, 143 contentMarkdown: p.content_markdown, 144 contentWarning: p.content_warning ?? '', 145 tags: tagsByPostUri.get(p.uri) ?? [] 146 }, 147 createdAt: new Date(p.created_at).toISOString(), 148 updatedAt: new Date(p.updated_at).toISOString() 149 }) satisfies AppWafrnContentDefs.PublicPostView 150 ) 151 152 const privatePosts = privatePostRows.map( 153 (p) => 154 ({ 155 $type: 'app.wafrn.content.defs#privatePostView', 156 uri: p.uri as ResourceUri, 157 visibility: p.visibility, 158 keyVersion: p.key_version, 159 encryptedContent: p.encrypted_content, 160 createdAt: new Date(p.created_at).toISOString(), 161 updatedAt: new Date(p.updated_at).toISOString() 162 }) satisfies AppWafrnContentDefs.PrivatePostView 163 ) 164 165 // Step 5: Merge and sort by creation time (most recent first) 166 const posts = [...publicPosts, ...privatePosts].sort((a, b) => { 167 return Number(a.createdAt < b.createdAt) 168 }) 169 170 return posts 171}