a tool for shared writing and social publishing

add get profile posts rpc

+100 -55
+35 -55
app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts
··· 20 20 ): Promise<{ posts: Post[]; nextCursor: Cursor | null }> { 21 21 const limit = 20; 22 22 23 - let query = supabaseServerClient 24 - .from("documents") 25 - .select( 26 - `*, 27 - comments_on_documents(count), 28 - document_mentions_in_bsky(count), 29 - recommends_on_documents(count), 30 - documents_in_publications(publications(*))`, 31 - ) 32 - .like("uri", `at://${did}/%`) 33 - .order("sort_date", { ascending: false }) 34 - .order("uri", { ascending: false }) 35 - .limit(limit); 23 + let [{ data: rawFeed, error }, { data: profile }] = await Promise.all([ 24 + supabaseServerClient.rpc("get_profile_posts", { 25 + p_did: did, 26 + p_cursor_sort_date: cursor?.sort_date ?? null, 27 + p_cursor_uri: cursor?.uri ?? null, 28 + p_limit: limit, 29 + }), 30 + supabaseServerClient 31 + .from("bsky_profiles") 32 + .select("handle") 33 + .eq("did", did) 34 + .single(), 35 + ]); 36 36 37 - if (cursor) { 38 - query = query.or( 39 - `sort_date.lt.${cursor.sort_date},and(sort_date.eq.${cursor.sort_date},uri.lt.${cursor.uri})`, 40 - ); 37 + if (error) { 38 + console.error("[getProfilePosts] rpc error:", error); 39 + return { posts: [], nextCursor: null }; 41 40 } 42 41 43 - let [{ data: rawDocs }, { data: rawPubs }, { data: profile }] = 44 - await Promise.all([ 45 - query, 46 - supabaseServerClient 47 - .from("publications") 48 - .select("*") 49 - .eq("identity_did", did), 50 - supabaseServerClient 51 - .from("bsky_profiles") 52 - .select("handle") 53 - .eq("did", did) 54 - .single(), 55 - ]); 56 - 57 - // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces 58 - const docs = deduplicateByUriOrdered(rawDocs || []); 59 - const pubs = deduplicateByUriOrdered(rawPubs || []); 42 + let feed = deduplicateByUriOrdered(rawFeed || []); 43 + if (feed.length === 0) return { posts: [], nextCursor: null }; 60 44 61 - // Build a map of publications for quick lookup 62 - let pubMap = new Map<string, NonNullable<typeof pubs>[number]>(); 63 - for (let pub of pubs || []) { 64 - pubMap.set(pub.uri, pub); 65 - } 66 - 67 - // Transform data to Post[] format 68 45 let handle = profile?.handle ? `@${profile.handle}` : null; 69 46 let posts: Post[] = []; 70 47 71 - for (let doc of docs || []) { 72 - // Normalize records - filter out unrecognized formats 73 - const normalizedData = normalizeDocumentRecord(doc.data, doc.uri); 48 + for (let row of feed) { 49 + const normalizedData = normalizeDocumentRecord(row.data, row.uri); 74 50 if (!normalizedData) continue; 75 51 76 - let pubFromDoc = doc.documents_in_publications?.[0]?.publications; 77 - let pub = pubFromDoc ? pubMap.get(pubFromDoc.uri) || pubFromDoc : null; 52 + const normalizedPubRecord = row.publication_record 53 + ? normalizePublicationRecord(row.publication_record) 54 + : null; 78 55 79 56 let post: Post = { 80 57 author: handle, 81 58 documents: { 82 59 data: normalizedData, 83 - uri: doc.uri, 84 - sort_date: doc.sort_date, 85 - comments_on_documents: doc.comments_on_documents, 86 - document_mentions_in_bsky: doc.document_mentions_in_bsky, 87 - recommends_on_documents: doc.recommends_on_documents, 60 + uri: row.uri, 61 + sort_date: row.sort_date, 62 + comments_on_documents: [{ count: Number(row.comments_count) }], 63 + document_mentions_in_bsky: [{ count: Number(row.mentions_count) }], 64 + recommends_on_documents: [{ count: Number(row.recommends_count) }], 88 65 }, 89 66 }; 90 67 91 - if (pub) { 68 + if (row.publication_uri) { 92 69 post.publication = { 93 - href: getPublicationURL(pub), 94 - pubRecord: normalizePublicationRecord(pub.record), 95 - uri: pub.uri, 70 + href: getPublicationURL({ 71 + uri: row.publication_uri, 72 + record: row.publication_record, 73 + }), 74 + pubRecord: normalizedPubRecord, 75 + uri: row.publication_uri, 96 76 }; 97 77 } 98 78
+19
supabase/database.types.ts
··· 1360 1360 like: unknown 1361 1361 }[] 1362 1362 } 1363 + get_profile_posts: { 1364 + Args: { 1365 + p_did: string 1366 + p_cursor_sort_date?: string | null 1367 + p_cursor_uri?: string | null 1368 + p_limit?: number 1369 + } 1370 + Returns: { 1371 + uri: string 1372 + data: Json 1373 + sort_date: string 1374 + comments_count: number 1375 + mentions_count: number 1376 + recommends_count: number 1377 + publication_uri: string 1378 + publication_record: Json 1379 + publication_name: string 1380 + }[] 1381 + } 1363 1382 get_reader_feed: { 1364 1383 Args: { 1365 1384 p_identity: string
+46
supabase/migrations/20260305000000_add_get_profile_posts_function.sql
··· 1 + CREATE OR REPLACE FUNCTION get_profile_posts( 2 + p_did text, 3 + p_cursor_sort_date timestamptz DEFAULT NULL, 4 + p_cursor_uri text DEFAULT NULL, 5 + p_limit int DEFAULT 20 6 + ) 7 + RETURNS TABLE ( 8 + uri text, 9 + data jsonb, 10 + sort_date timestamptz, 11 + comments_count bigint, 12 + mentions_count bigint, 13 + recommends_count bigint, 14 + publication_uri text, 15 + publication_record jsonb, 16 + publication_name text 17 + ) 18 + LANGUAGE sql STABLE 19 + AS $$ 20 + SELECT 21 + d.uri, 22 + d.data, 23 + d.sort_date, 24 + (SELECT count(*) FROM comments_on_documents c WHERE c.document = d.uri), 25 + (SELECT count(*) FROM document_mentions_in_bsky m WHERE m.document = d.uri), 26 + (SELECT count(*) FROM recommends_on_documents r WHERE r.document = d.uri), 27 + pub.uri, 28 + pub.record, 29 + pub.name 30 + FROM documents d 31 + LEFT JOIN LATERAL ( 32 + SELECT p.uri, p.record, p.name 33 + FROM documents_in_publications dip 34 + JOIN publications p ON p.uri = dip.publication 35 + WHERE dip.document = d.uri 36 + LIMIT 1 37 + ) pub ON true 38 + WHERE d.uri LIKE 'at://' || p_did || '/%' 39 + AND ( 40 + p_cursor_sort_date IS NULL 41 + OR d.sort_date < p_cursor_sort_date 42 + OR (d.sort_date = p_cursor_sort_date AND d.uri < p_cursor_uri) 43 + ) 44 + ORDER BY d.sort_date DESC, d.uri DESC 45 + LIMIT p_limit; 46 + $$;