Testing implementation for private data in ATProto with ATPKeyserver and ATCute tools
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}