a tool for shared writing and social publishing

add fix incorrect site values function

+307
+5
app/api/inngest/client.ts
··· 41 41 documentUris: string[]; 42 42 }; 43 43 }; 44 + "documents/fix-incorrect-site-values": { 45 + data: { 46 + did: string; 47 + }; 48 + }; 44 49 }; 45 50 46 51 // Create a client to send and receive events
+300
app/api/inngest/functions/fix_incorrect_site_values.ts
··· 1 + import { supabaseServerClient } from "supabase/serverClient"; 2 + import { inngest } from "../client"; 3 + import { restoreOAuthSession } from "src/atproto-oauth"; 4 + import { AtpBaseClient, SiteStandardDocument } from "lexicons/api"; 5 + import { AtUri } from "@atproto/syntax"; 6 + import { Json } from "supabase/database.types"; 7 + 8 + async function createAuthenticatedAgent(did: string): Promise<AtpBaseClient> { 9 + const result = await restoreOAuthSession(did); 10 + if (!result.ok) { 11 + throw new Error(`Failed to restore OAuth session: ${result.error.message}`); 12 + } 13 + const credentialSession = result.value; 14 + return new AtpBaseClient( 15 + credentialSession.fetchHandler.bind(credentialSession), 16 + ); 17 + } 18 + 19 + /** 20 + * Build set of valid site values for a publication. 21 + * A site value is valid if it matches the publication or its legacy equivalent. 22 + */ 23 + function buildValidSiteValues(pubUri: string): Set<string> { 24 + const validValues = new Set<string>([pubUri]); 25 + 26 + try { 27 + const aturi = new AtUri(pubUri); 28 + 29 + if (pubUri.includes("/site.standard.publication/")) { 30 + // Also accept legacy pub.leaflet.publication 31 + validValues.add( 32 + `at://${aturi.hostname}/pub.leaflet.publication/${aturi.rkey}`, 33 + ); 34 + } else if (pubUri.includes("/pub.leaflet.publication/")) { 35 + // Also accept new site.standard.publication 36 + validValues.add( 37 + `at://${aturi.hostname}/site.standard.publication/${aturi.rkey}`, 38 + ); 39 + } 40 + } catch (e) { 41 + // Invalid URI, just use the original 42 + } 43 + 44 + return validValues; 45 + } 46 + 47 + /** 48 + * This function finds and fixes documents that have incorrect site values. 49 + * A document has an incorrect site value if its `site` field doesn't match 50 + * the publication it belongs to (via documents_in_publications). 51 + * 52 + * Takes a DID as input and processes publications owned by that identity. 53 + */ 54 + export const fix_incorrect_site_values = inngest.createFunction( 55 + { id: "fix_incorrect_site_values" }, 56 + { event: "documents/fix-incorrect-site-values" }, 57 + async ({ event, step }) => { 58 + const { did } = event.data; 59 + 60 + const stats = { 61 + publicationsChecked: 0, 62 + documentsChecked: 0, 63 + documentsWithIncorrectSite: 0, 64 + documentsFixed: 0, 65 + documentsMissingSite: 0, 66 + errors: [] as string[], 67 + }; 68 + 69 + // Step 1: Get all publications owned by this identity 70 + const publications = await step.run("fetch-publications", async () => { 71 + const { data, error } = await supabaseServerClient 72 + .from("publications") 73 + .select("uri") 74 + .eq("identity_did", did); 75 + 76 + if (error) { 77 + throw new Error(`Failed to fetch publications: ${error.message}`); 78 + } 79 + return data || []; 80 + }); 81 + 82 + stats.publicationsChecked = publications.length; 83 + 84 + if (publications.length === 0) { 85 + return { 86 + success: true, 87 + message: "No publications found for this identity", 88 + stats, 89 + }; 90 + } 91 + 92 + // Step 2: Get all documents_in_publications entries for these publications 93 + const publicationUris = publications.map((p) => p.uri); 94 + 95 + const joinEntries = await step.run( 96 + "fetch-documents-in-publications", 97 + async () => { 98 + const { data, error } = await supabaseServerClient 99 + .from("documents_in_publications") 100 + .select("document, publication") 101 + .in("publication", publicationUris); 102 + 103 + if (error) { 104 + throw new Error( 105 + `Failed to fetch documents_in_publications: ${error.message}`, 106 + ); 107 + } 108 + return data || []; 109 + }, 110 + ); 111 + 112 + if (joinEntries.length === 0) { 113 + return { 114 + success: true, 115 + message: "No documents found in publications", 116 + stats, 117 + }; 118 + } 119 + 120 + // Create a map of document URI -> expected publication URI 121 + const documentToPublication = new Map<string, string>(); 122 + for (const row of joinEntries) { 123 + documentToPublication.set(row.document, row.publication); 124 + } 125 + 126 + // Step 3: Fetch all document records 127 + const documentUris = Array.from(documentToPublication.keys()); 128 + 129 + const allDocuments = await step.run("fetch-documents", async () => { 130 + const { data, error } = await supabaseServerClient 131 + .from("documents") 132 + .select("uri, data") 133 + .in("uri", documentUris); 134 + 135 + if (error) { 136 + throw new Error(`Failed to fetch documents: ${error.message}`); 137 + } 138 + return data || []; 139 + }); 140 + 141 + stats.documentsChecked = allDocuments.length; 142 + 143 + // Step 4: Find documents with incorrect site values 144 + const documentsToFix: Array<{ 145 + uri: string; 146 + currentSite: string | null; 147 + correctSite: string; 148 + docData: SiteStandardDocument.Record; 149 + }> = []; 150 + 151 + for (const doc of allDocuments) { 152 + const expectedPubUri = documentToPublication.get(doc.uri); 153 + if (!expectedPubUri) continue; 154 + 155 + const data = doc.data as unknown as SiteStandardDocument.Record; 156 + const currentSite = data?.site; 157 + 158 + if (!currentSite) { 159 + stats.documentsMissingSite++; 160 + continue; 161 + } 162 + 163 + const validSiteValues = buildValidSiteValues(expectedPubUri); 164 + 165 + if (!validSiteValues.has(currentSite)) { 166 + // Document has incorrect site value - determine the correct one 167 + // Prefer the site.standard.publication format if the doc is site.standard.document 168 + let correctSite = expectedPubUri; 169 + 170 + if (doc.uri.includes("/site.standard.document/")) { 171 + // For site.standard.document, use site.standard.publication format 172 + try { 173 + const pubAturi = new AtUri(expectedPubUri); 174 + if (expectedPubUri.includes("/pub.leaflet.publication/")) { 175 + correctSite = `at://${pubAturi.hostname}/site.standard.publication/${pubAturi.rkey}`; 176 + } 177 + } catch (e) { 178 + // Use as-is 179 + } 180 + } 181 + 182 + documentsToFix.push({ 183 + uri: doc.uri, 184 + currentSite, 185 + correctSite, 186 + docData: data, 187 + }); 188 + } 189 + } 190 + 191 + stats.documentsWithIncorrectSite = documentsToFix.length; 192 + 193 + if (documentsToFix.length === 0) { 194 + return { 195 + success: true, 196 + message: "All documents have correct site values", 197 + stats, 198 + }; 199 + } 200 + 201 + // Step 5: Group documents by author DID for efficient OAuth session handling 202 + const docsByDid = new Map<string, typeof documentsToFix>(); 203 + for (const doc of documentsToFix) { 204 + try { 205 + const aturi = new AtUri(doc.uri); 206 + const authorDid = aturi.hostname; 207 + const existing = docsByDid.get(authorDid) || []; 208 + existing.push(doc); 209 + docsByDid.set(authorDid, existing); 210 + } catch (e) { 211 + stats.errors.push(`Invalid URI: ${doc.uri}`); 212 + } 213 + } 214 + 215 + // Step 6: Process each author's documents 216 + for (const [authorDid, docs] of docsByDid) { 217 + // Verify OAuth session for this author 218 + const oauthValid = await step.run( 219 + `verify-oauth-${authorDid.slice(-8)}`, 220 + async () => { 221 + const result = await restoreOAuthSession(authorDid); 222 + return result.ok; 223 + }, 224 + ); 225 + 226 + if (!oauthValid) { 227 + stats.errors.push(`No valid OAuth session for ${authorDid}`); 228 + continue; 229 + } 230 + 231 + // Fix each document for this author 232 + for (const docToFix of docs) { 233 + const result = await step.run( 234 + `fix-doc-${docToFix.uri.slice(-12)}`, 235 + async () => { 236 + try { 237 + const docAturi = new AtUri(docToFix.uri); 238 + 239 + // Build updated record 240 + const updatedRecord: SiteStandardDocument.Record = { 241 + ...docToFix.docData, 242 + site: docToFix.correctSite, 243 + }; 244 + 245 + // Update on PDS 246 + const agent = await createAuthenticatedAgent(authorDid); 247 + await agent.com.atproto.repo.putRecord({ 248 + repo: authorDid, 249 + collection: docAturi.collection, 250 + rkey: docAturi.rkey, 251 + record: updatedRecord, 252 + validate: false, 253 + }); 254 + 255 + // Update in database 256 + const { error: dbError } = await supabaseServerClient 257 + .from("documents") 258 + .update({ data: updatedRecord as Json }) 259 + .eq("uri", docToFix.uri); 260 + 261 + if (dbError) { 262 + return { 263 + success: false as const, 264 + error: `Database update failed: ${dbError.message}`, 265 + }; 266 + } 267 + 268 + return { 269 + success: true as const, 270 + oldSite: docToFix.currentSite, 271 + newSite: docToFix.correctSite, 272 + }; 273 + } catch (e) { 274 + return { 275 + success: false as const, 276 + error: e instanceof Error ? e.message : String(e), 277 + }; 278 + } 279 + }, 280 + ); 281 + 282 + if (result.success) { 283 + stats.documentsFixed++; 284 + } else { 285 + stats.errors.push(`${docToFix.uri}: ${result.error}`); 286 + } 287 + } 288 + } 289 + 290 + return { 291 + success: stats.errors.length === 0, 292 + stats, 293 + documentsToFix: documentsToFix.map((d) => ({ 294 + uri: d.uri, 295 + oldSite: d.currentSite, 296 + newSite: d.correctSite, 297 + })), 298 + }; 299 + }, 300 + );
+2
app/api/inngest/route.tsx
··· 6 6 import { index_follows } from "./functions/index_follows"; 7 7 import { migrate_user_to_standard } from "./functions/migrate_user_to_standard"; 8 8 import { fix_standard_document_publications } from "./functions/fix_standard_document_publications"; 9 + import { fix_incorrect_site_values } from "./functions/fix_incorrect_site_values"; 9 10 import { 10 11 cleanup_expired_oauth_sessions, 11 12 check_oauth_session, ··· 20 21 index_follows, 21 22 migrate_user_to_standard, 22 23 fix_standard_document_publications, 24 + fix_incorrect_site_values, 23 25 cleanup_expired_oauth_sessions, 24 26 check_oauth_session, 25 27 ],