a tool for shared writing and social publishing

fix some lexicon validation stuff

+75 -58
+30 -48
app/api/inngest/functions/migrate_user_to_standard.ts
··· 109 }) 110 .filter((x) => x !== null); 111 112 - // Run all PDS writes in parallel 113 - const pubPdsResults = await Promise.all( 114 - publicationsToMigrate.map(({ pub, rkey, newRecord }) => 115 - step.run(`pds-write-publication-${pub.uri}`, async () => { 116 const agent = await createAuthenticatedAgent(did); 117 const putResult = await agent.com.atproto.repo.putRecord({ 118 repo: did, ··· 121 record: newRecord, 122 validate: false, 123 }); 124 - return { oldUri: pub.uri, newUri: putResult.data.uri }; 125 - }), 126 - ), 127 - ); 128 129 - // Run all DB writes in parallel 130 - const pubDbResults = await Promise.all( 131 - publicationsToMigrate.map(({ pub, normalized, newRecord }, index) => { 132 - const newUri = pubPdsResults[index].newUri; 133 - return step.run(`db-write-publication-${pub.uri}`, async () => { 134 const { error: dbError } = await supabaseServerClient 135 .from("publications") 136 .upsert({ ··· 149 }; 150 } 151 return { success: true as const, oldUri: pub.uri, newUri }; 152 - }); 153 - }), 154 ); 155 156 // Process results 157 - for (const result of pubDbResults) { 158 if (result.success) { 159 publicationUriMap[result.oldUri] = result.newUri; 160 stats.publicationsMigrated++; ··· 239 $type: "site.standard.document", 240 title: normalized.title || "Untitled", 241 site: siteValue, 242 - path: rkey, 243 publishedAt: normalized.publishedAt || new Date().toISOString(), 244 description: normalized.description, 245 content: normalized.content, ··· 252 }) 253 .filter((x) => x !== null); 254 255 - // Run all PDS writes in parallel 256 - const docPdsResults = await Promise.all( 257 - documentsToMigrate.map(({ doc, rkey, newRecord }) => 258 - step.run(`pds-write-document-${doc.uri}`, async () => { 259 const agent = await createAuthenticatedAgent(did); 260 const putResult = await agent.com.atproto.repo.putRecord({ 261 repo: did, ··· 264 record: newRecord, 265 validate: false, 266 }); 267 - return { oldUri: doc.uri, newUri: putResult.data.uri }; 268 - }), 269 - ), 270 - ); 271 272 - // Run all DB writes in parallel 273 - const docDbResults = await Promise.all( 274 - documentsToMigrate.map(({ doc, newRecord, oldPubUri }, index) => { 275 - const newUri = docPdsResults[index].newUri; 276 - return step.run(`db-write-document-${doc.uri}`, async () => { 277 const { error: dbError } = await supabaseServerClient 278 .from("documents") 279 .upsert({ ··· 302 } 303 304 return { success: true as const, oldUri: doc.uri, newUri }; 305 - }); 306 - }), 307 ); 308 309 // Process results 310 - for (const result of docDbResults) { 311 if (result.success) { 312 documentUriMap[result.oldUri] = result.newUri; 313 stats.documentsMigrated++; ··· 428 }) 429 .filter((x) => x !== null); 430 431 - // Run all PDS writes in parallel 432 - const subPdsResults = await Promise.all( 433 subscriptionsToMigrate.map(({ sub, rkey, newRecord }) => 434 - step.run(`pds-write-subscription-${sub.uri}`, async () => { 435 const agent = await createAuthenticatedAgent(did); 436 const putResult = await agent.com.atproto.repo.putRecord({ 437 repo: did, ··· 440 record: newRecord, 441 validate: false, 442 }); 443 - return { oldUri: sub.uri, newUri: putResult.data.uri }; 444 - }), 445 - ), 446 - ); 447 448 - // Run all DB writes in parallel 449 - const subDbResults = await Promise.all( 450 - subscriptionsToMigrate.map(({ sub, newRecord }, index) => { 451 - const newUri = subPdsResults[index].newUri; 452 - return step.run(`db-write-subscription-${sub.uri}`, async () => { 453 const { error: dbError } = await supabaseServerClient 454 .from("publication_subscriptions") 455 .update({ ··· 467 }; 468 } 469 return { success: true as const, oldUri: sub.uri, newUri }; 470 - }); 471 - }), 472 ); 473 474 // Process results 475 - for (const result of subDbResults) { 476 if (result.success) { 477 userSubscriptionUriMap[result.oldUri] = result.newUri; 478 stats.userSubscriptionsMigrated++;
··· 109 }) 110 .filter((x) => x !== null); 111 112 + // Run PDS + DB writes together for each publication 113 + const pubResults = await Promise.all( 114 + publicationsToMigrate.map(({ pub, rkey, normalized, newRecord }) => 115 + step.run(`migrate-publication-${pub.uri}`, async () => { 116 + // PDS write 117 const agent = await createAuthenticatedAgent(did); 118 const putResult = await agent.com.atproto.repo.putRecord({ 119 repo: did, ··· 122 record: newRecord, 123 validate: false, 124 }); 125 + const newUri = putResult.data.uri; 126 127 + // DB write 128 const { error: dbError } = await supabaseServerClient 129 .from("publications") 130 .upsert({ ··· 143 }; 144 } 145 return { success: true as const, oldUri: pub.uri, newUri }; 146 + }), 147 + ), 148 ); 149 150 // Process results 151 + for (const result of pubResults) { 152 if (result.success) { 153 publicationUriMap[result.oldUri] = result.newUri; 154 stats.publicationsMigrated++; ··· 233 $type: "site.standard.document", 234 title: normalized.title || "Untitled", 235 site: siteValue, 236 + path: "/" + rkey, 237 publishedAt: normalized.publishedAt || new Date().toISOString(), 238 description: normalized.description, 239 content: normalized.content, ··· 246 }) 247 .filter((x) => x !== null); 248 249 + // Run PDS + DB writes together for each document 250 + const docResults = await Promise.all( 251 + documentsToMigrate.map(({ doc, rkey, newRecord, oldPubUri }) => 252 + step.run(`migrate-document-${doc.uri}`, async () => { 253 + // PDS write 254 const agent = await createAuthenticatedAgent(did); 255 const putResult = await agent.com.atproto.repo.putRecord({ 256 repo: did, ··· 259 record: newRecord, 260 validate: false, 261 }); 262 + const newUri = putResult.data.uri; 263 264 + // DB write 265 const { error: dbError } = await supabaseServerClient 266 .from("documents") 267 .upsert({ ··· 290 } 291 292 return { success: true as const, oldUri: doc.uri, newUri }; 293 + }), 294 + ), 295 ); 296 297 // Process results 298 + for (const result of docResults) { 299 if (result.success) { 300 documentUriMap[result.oldUri] = result.newUri; 301 stats.documentsMigrated++; ··· 416 }) 417 .filter((x) => x !== null); 418 419 + // Run PDS + DB writes together for each subscription 420 + const subResults = await Promise.all( 421 subscriptionsToMigrate.map(({ sub, rkey, newRecord }) => 422 + step.run(`migrate-subscription-${sub.uri}`, async () => { 423 + // PDS write 424 const agent = await createAuthenticatedAgent(did); 425 const putResult = await agent.com.atproto.repo.putRecord({ 426 repo: did, ··· 429 record: newRecord, 430 validate: false, 431 }); 432 + const newUri = putResult.data.uri; 433 434 + // DB write 435 const { error: dbError } = await supabaseServerClient 436 .from("publication_subscriptions") 437 .update({ ··· 449 }; 450 } 451 return { success: true as const, oldUri: sub.uri, newUri }; 452 + }), 453 + ), 454 ); 455 456 // Process results 457 + for (const result of subResults) { 458 if (result.success) { 459 userSubscriptionUriMap[result.oldUri] = result.newUri; 460 stats.userSubscriptionsMigrated++;
+2 -2
lexicons/api/lexicons.ts
··· 2215 type: 'ref', 2216 }, 2217 theme: { 2218 - type: 'ref', 2219 - ref: 'lex:pub.leaflet.publication#theme', 2220 }, 2221 description: { 2222 maxGraphemes: 300,
··· 2215 type: 'ref', 2216 }, 2217 theme: { 2218 + type: 'union', 2219 + refs: ['lex:pub.leaflet.publication#theme'], 2220 }, 2221 description: { 2222 maxGraphemes: 300,
+1 -1
lexicons/api/types/site/standard/publication.ts
··· 15 export interface Record { 16 $type: 'site.standard.publication' 17 basicTheme?: SiteStandardThemeBasic.Main 18 - theme?: PubLeafletPublication.Theme 19 description?: string 20 icon?: BlobRef 21 name: string
··· 15 export interface Record { 16 $type: 'site.standard.publication' 17 basicTheme?: SiteStandardThemeBasic.Main 18 + theme?: $Typed<PubLeafletPublication.Theme> | { $type: string } 19 description?: string 20 icon?: BlobRef 21 name: string
+2 -2
lexicons/site/standard/publication.json
··· 9 "type": "ref" 10 }, 11 "theme": { 12 - "type": "ref", 13 - "ref": "pub.leaflet.publication#theme" 14 }, 15 "description": { 16 "maxGraphemes": 300,
··· 9 "type": "ref" 10 }, 11 "theme": { 12 + "type": "union", 13 + "refs": ["pub.leaflet.publication#theme"] 14 }, 15 "description": { 16 "maxGraphemes": 300,
+40 -5
lexicons/src/normalize.ts
··· 14 */ 15 16 import type * as PubLeafletDocument from "../api/types/pub/leaflet/document"; 17 - import type * as PubLeafletPublication from "../api/types/pub/leaflet/publication"; 18 import type * as PubLeafletContent from "../api/types/pub/leaflet/content"; 19 import type * as SiteStandardDocument from "../api/types/site/standard/document"; 20 import type * as SiteStandardPublication from "../api/types/site/standard/publication"; ··· 31 }; 32 33 // Normalized publication type - uses the generated site.standard.publication type 34 - export type NormalizedPublication = SiteStandardPublication.Record; 35 36 /** 37 * Checks if the record is a pub.leaflet.document ··· 210 ): NormalizedPublication | null { 211 if (!record || typeof record !== "object") return null; 212 213 - // Pass through site.standard records directly 214 if (isStandardPublication(record)) { 215 - return record; 216 } 217 218 if (isLeafletPublication(record)) { ··· 225 226 const basicTheme = leafletThemeToBasicTheme(record.theme); 227 228 // Convert preferences to site.standard format (strip/replace $type) 229 const preferences: SiteStandardPublication.Preferences | undefined = 230 record.preferences ··· 243 description: record.description, 244 icon: record.icon, 245 basicTheme, 246 - theme: record.theme, 247 preferences, 248 }; 249 }
··· 14 */ 15 16 import type * as PubLeafletDocument from "../api/types/pub/leaflet/document"; 17 + import * as PubLeafletPublication from "../api/types/pub/leaflet/publication"; 18 import type * as PubLeafletContent from "../api/types/pub/leaflet/content"; 19 import type * as SiteStandardDocument from "../api/types/site/standard/document"; 20 import type * as SiteStandardPublication from "../api/types/site/standard/publication"; ··· 31 }; 32 33 // Normalized publication type - uses the generated site.standard.publication type 34 + // with the theme narrowed to only the valid pub.leaflet.publication#theme type 35 + // (isTheme validates that $type is present, so we use $Typed) 36 + // Note: We explicitly list fields rather than using Omit because the generated Record type 37 + // has an index signature [k: string]: unknown that interferes with property typing 38 + export type NormalizedPublication = { 39 + $type: "site.standard.publication"; 40 + name: string; 41 + url: string; 42 + description?: string; 43 + icon?: SiteStandardPublication.Record["icon"]; 44 + basicTheme?: SiteStandardThemeBasic.Main; 45 + theme?: $Typed<PubLeafletPublication.Theme>; 46 + preferences?: SiteStandardPublication.Preferences; 47 + }; 48 49 /** 50 * Checks if the record is a pub.leaflet.document ··· 223 ): NormalizedPublication | null { 224 if (!record || typeof record !== "object") return null; 225 226 + // Pass through site.standard records directly, but validate the theme 227 if (isStandardPublication(record)) { 228 + // Validate theme - only keep if it's a valid pub.leaflet.publication#theme 229 + const theme = PubLeafletPublication.isTheme(record.theme) 230 + ? (record.theme as $Typed<PubLeafletPublication.Theme>) 231 + : undefined; 232 + return { 233 + ...record, 234 + theme, 235 + }; 236 } 237 238 if (isLeafletPublication(record)) { ··· 245 246 const basicTheme = leafletThemeToBasicTheme(record.theme); 247 248 + // Validate theme - only keep if it's a valid pub.leaflet.publication#theme with $type set 249 + // For legacy records without $type, add it during normalization 250 + let theme: $Typed<PubLeafletPublication.Theme> | undefined; 251 + if (record.theme) { 252 + if (PubLeafletPublication.isTheme(record.theme)) { 253 + theme = record.theme as $Typed<PubLeafletPublication.Theme>; 254 + } else { 255 + // Legacy theme without $type - add it 256 + theme = { 257 + ...record.theme, 258 + $type: "pub.leaflet.publication#theme", 259 + }; 260 + } 261 + } 262 + 263 // Convert preferences to site.standard format (strip/replace $type) 264 const preferences: SiteStandardPublication.Preferences | undefined = 265 record.preferences ··· 278 description: record.description, 279 icon: record.icon, 280 basicTheme, 281 + theme, 282 preferences, 283 }; 284 }