a tool for shared writing and social publishing
at 8541ab22feb4b80e153ef60c43aabc133b7d463b 215 lines 6.5 kB view raw
1import { createClient } from "@supabase/supabase-js"; 2import { Database, Json } from "supabase/database.types"; 3import { jsonToLex } from "@atproto/lexicon"; 4import { 5 PubLeafletDocument, 6 SiteStandardDocument, 7} from "lexicons/api"; 8 9const supabase = createClient<Database>( 10 process.env.NEXT_PUBLIC_SUPABASE_API_URL as string, 11 process.env.SUPABASE_SERVICE_ROLE_KEY as string, 12); 13 14const BATCH_SIZE = 100; 15const DRY_RUN = process.argv.includes("--dry-run"); 16 17interface ValidationIssue { 18 uri: string; 19 error: string; 20 fixed?: boolean; 21} 22 23// Fields that should be integers based on lexicon schemas 24const INTEGER_FIELDS = new Set([ 25 "x", "y", "width", "height", "rotation", "offset", "level", 26 "r", "g", "b", "a", 27]); 28 29/** 30 * Recursively fix float values that should be integers 31 */ 32function fixFloatsToIntegers(obj: unknown): { value: unknown; modified: boolean } { 33 if (obj === null || obj === undefined) { 34 return { value: obj, modified: false }; 35 } 36 37 if (Array.isArray(obj)) { 38 let modified = false; 39 const newArr = obj.map((item) => { 40 const result = fixFloatsToIntegers(item); 41 if (result.modified) modified = true; 42 return result.value; 43 }); 44 return { value: newArr, modified }; 45 } 46 47 if (typeof obj === "object") { 48 let modified = false; 49 const newObj: Record<string, unknown> = {}; 50 51 for (const [key, value] of Object.entries(obj as Record<string, unknown>)) { 52 if (INTEGER_FIELDS.has(key) && typeof value === "number" && !Number.isInteger(value)) { 53 newObj[key] = Math.round(value); 54 modified = true; 55 } else { 56 const result = fixFloatsToIntegers(value); 57 newObj[key] = result.value; 58 if (result.modified) modified = true; 59 } 60 } 61 62 return { value: newObj, modified }; 63 } 64 65 return { value: obj, modified: false }; 66} 67 68async function validateDocuments(): Promise<void> { 69 console.log("=".repeat(60)); 70 console.log("Document Validation Script"); 71 console.log(`Mode: ${DRY_RUN ? "DRY RUN" : "FIX MODE"}`); 72 console.log("=".repeat(60)); 73 74 const issues: ValidationIssue[] = []; 75 let offset = 0; 76 let totalProcessed = 0; 77 let totalWithIssues = 0; 78 let totalFixed = 0; 79 80 console.log("\nFetching documents in batches...\n"); 81 82 while (true) { 83 const { data: documents, error } = await supabase 84 .from("documents") 85 .select("uri, data") 86 .range(offset, offset + BATCH_SIZE - 1) 87 .order("uri"); 88 89 if (error) { 90 console.error("Error fetching documents:", error); 91 break; 92 } 93 94 if (!documents || documents.length === 0) { 95 break; 96 } 97 98 for (const doc of documents) { 99 totalProcessed++; 100 101 const rawData = doc.data; 102 if (!rawData || typeof rawData !== "object") { 103 issues.push({ uri: doc.uri, error: "Document data is null or not an object" }); 104 totalWithIssues++; 105 continue; 106 } 107 108 // Convert JSON to lexicon types (handles BlobRef conversion) 109 const data = jsonToLex(rawData); 110 111 // Determine document type and validate 112 const $type = (data as Record<string, unknown>).$type; 113 114 let result: { success: boolean; error?: unknown }; 115 let validateFn: (v: unknown) => { success: boolean; error?: unknown }; 116 117 if ($type === "site.standard.document") { 118 validateFn = SiteStandardDocument.validateRecord; 119 result = validateFn(data); 120 } else if ( 121 $type === "pub.leaflet.document" || 122 // Legacy records without $type but with pages array 123 (Array.isArray((data as Record<string, unknown>).pages) && 124 typeof (data as Record<string, unknown>).author === "string") 125 ) { 126 validateFn = PubLeafletDocument.validateRecord; 127 result = validateFn(data); 128 } else { 129 issues.push({ uri: doc.uri, error: `Unknown document type: ${$type || "undefined"}` }); 130 totalWithIssues++; 131 continue; 132 } 133 134 if (!result.success) { 135 const errorStr = String(result.error); 136 137 // Check if it's an integer validation error we can fix 138 if (errorStr.includes("must be an integer")) { 139 const { value: fixedData, modified } = fixFloatsToIntegers(rawData); 140 141 if (modified) { 142 // Validate the fixed data 143 const fixedResult = validateFn(jsonToLex(fixedData)); 144 145 if (fixedResult.success) { 146 if (!DRY_RUN) { 147 const { error: updateError } = await supabase 148 .from("documents") 149 .update({ data: fixedData as Json }) 150 .eq("uri", doc.uri); 151 152 if (updateError) { 153 issues.push({ uri: doc.uri, error: `Fix failed: ${updateError.message}` }); 154 totalWithIssues++; 155 } else { 156 issues.push({ uri: doc.uri, error: errorStr, fixed: true }); 157 totalFixed++; 158 } 159 } else { 160 issues.push({ uri: doc.uri, error: errorStr, fixed: true }); 161 totalFixed++; 162 } 163 } else { 164 // Still has issues after fix 165 issues.push({ uri: doc.uri, error: String(fixedResult.error) }); 166 totalWithIssues++; 167 } 168 } else { 169 issues.push({ uri: doc.uri, error: errorStr }); 170 totalWithIssues++; 171 } 172 } else { 173 issues.push({ uri: doc.uri, error: errorStr }); 174 totalWithIssues++; 175 } 176 } 177 } 178 179 process.stdout.write( 180 `\rProcessed ${totalProcessed} documents, ${totalFixed} fixed, ${totalWithIssues} with issues...`, 181 ); 182 offset += BATCH_SIZE; 183 } 184 185 console.log("\n"); 186 187 // Print summary 188 console.log("=".repeat(60)); 189 console.log("VALIDATION SUMMARY"); 190 console.log("=".repeat(60)); 191 console.log(`Total documents processed: ${totalProcessed}`); 192 console.log(`Documents fixed: ${totalFixed}${DRY_RUN ? " (dry run)" : ""}`); 193 console.log(`Documents with unfixable issues: ${totalWithIssues}`); 194 console.log(""); 195 196 if (issues.length > 0) { 197 console.log("DETAILS:"); 198 console.log("-".repeat(60)); 199 200 for (const issue of issues) { 201 const status = issue.fixed ? "[FIXED]" : "[ERROR]"; 202 console.log(`\n${status} ${issue.uri}`); 203 console.log(` ${issue.error}`); 204 } 205 } else { 206 console.log("No issues found!"); 207 } 208 209 console.log("\n" + "=".repeat(60)); 210} 211 212validateDocuments().catch((err) => { 213 console.error("Fatal error:", err); 214 process.exit(1); 215});