The Appview for the kipclip.com atproto bookmarking service
at main 111 lines 3.5 kB view raw
1/** 2 * Background migration: moves $enriched data from bookmark records 3 * to annotation sidecar records. 4 * 5 * Triggered fire-and-forget from /api/initial-data after the response is sent. 6 * Only runs if the annotation scope is available. 7 */ 8 9import { ANNOTATION_COLLECTION, BOOKMARK_COLLECTION } from "./route-utils.ts"; 10import type { AnnotationRecord } from "../shared/types.ts"; 11 12const BATCH_SIZE = 5; 13 14/** 15 * Find bookmarks with $enriched data that lack a corresponding annotation, 16 * create the annotation, and clean the bookmark record. 17 */ 18export async function migrateAnnotations( 19 oauthSession: any, 20 bookmarkRecords: any[], 21 annotationMap: Map<string, AnnotationRecord>, 22): Promise<void> { 23 // Find bookmarks that need migration: 24 // - Have $enriched data or top-level title 25 // - Don't have a corresponding annotation 26 const needsMigration = bookmarkRecords.filter((record) => { 27 const rkey = record.uri.split("/").pop(); 28 if (!rkey) return false; 29 if (annotationMap.has(rkey)) return false; 30 return record.value.$enriched || record.value.title; 31 }); 32 33 if (needsMigration.length === 0) return; 34 35 console.log( 36 `Annotation migration: ${needsMigration.length} bookmarks to migrate`, 37 ); 38 39 // Process in batches to avoid rate limits 40 for (let i = 0; i < needsMigration.length; i += BATCH_SIZE) { 41 const batch = needsMigration.slice(i, i + BATCH_SIZE); 42 43 await Promise.all(batch.map(async (record: any) => { 44 const rkey = record.uri.split("/").pop(); 45 if (!rkey) return; 46 47 try { 48 const enriched = record.value.$enriched || {}; 49 50 // Create annotation sidecar 51 const annotation: AnnotationRecord = { 52 subject: record.uri, 53 title: enriched.title || record.value.title, 54 description: enriched.description, 55 favicon: enriched.favicon, 56 image: enriched.image, 57 createdAt: record.value.createdAt, 58 }; 59 60 const annResponse = await oauthSession.makeRequest( 61 "POST", 62 `${oauthSession.pdsUrl}/xrpc/com.atproto.repo.putRecord`, 63 { 64 headers: { "Content-Type": "application/json" }, 65 body: JSON.stringify({ 66 repo: oauthSession.did, 67 collection: ANNOTATION_COLLECTION, 68 rkey, 69 record: annotation, 70 }), 71 }, 72 ); 73 74 if (!annResponse.ok) { 75 console.error(`Migration: failed to create annotation for ${rkey}`); 76 return; 77 } 78 79 // Clean the bookmark record (remove $enriched and top-level title) 80 const cleanRecord: Record<string, unknown> = { 81 subject: record.value.subject, 82 createdAt: record.value.createdAt, 83 tags: record.value.tags || [], 84 }; 85 86 await oauthSession.makeRequest( 87 "POST", 88 `${oauthSession.pdsUrl}/xrpc/com.atproto.repo.putRecord`, 89 { 90 headers: { "Content-Type": "application/json" }, 91 body: JSON.stringify({ 92 repo: oauthSession.did, 93 collection: BOOKMARK_COLLECTION, 94 rkey, 95 record: cleanRecord, 96 }), 97 }, 98 ); 99 } catch (err) { 100 console.error(`Migration: error migrating bookmark ${rkey}:`, err); 101 } 102 })); 103 104 // Small delay between batches 105 if (i + BATCH_SIZE < needsMigration.length) { 106 await new Promise((resolve) => setTimeout(resolve, 200)); 107 } 108 } 109 110 console.log("Annotation migration: complete"); 111}