👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

use lexicon validation object to infer collection nsid

+39 -54
+39 -54
src/lib/atproto-client.ts
··· 20 20 type AtUri = `at://${string}`; 21 21 22 22 const SLINGSHOT_BASE = "https://slingshot.microcosm.blue"; 23 - const DECK_COLLECTION = "com.deckbelcher.deck.list" as const; 24 - const LIST_COLLECTION = "com.deckbelcher.collection.list" as const; 25 23 26 - type Collection = typeof DECK_COLLECTION | typeof LIST_COLLECTION; 24 + type Collection = `${string}.${string}.${string}`; 25 + 26 + function getCollectionFromSchema(schema: BaseSchema): Collection { 27 + // Schema structure: { object: { shape: { $type: { expected: "com.foo.bar" } } } } 28 + const schemaAny = schema as { 29 + object?: { shape?: { $type?: { expected?: string } } }; 30 + }; 31 + const collection = schemaAny.object?.shape?.$type?.expected; 32 + if (!collection) { 33 + throw new Error("Schema does not have $type.expected"); 34 + } 35 + return collection as Collection; 36 + } 27 37 28 38 // Branded types for type safety 29 39 declare const PdsUrlBrand: unique symbol; ··· 62 72 async function getRecord<TSchema extends BaseSchema>( 63 73 did: Did, 64 74 rkey: Rkey, 65 - collection: Collection, 66 - entityName: string, 67 75 schema: TSchema, 68 76 ): Promise<Result<RecordResponse<InferOutput<TSchema>>>> { 77 + const collection = getCollectionFromSchema(schema); 69 78 try { 70 79 const url = new URL(`${SLINGSHOT_BASE}/xrpc/com.atproto.repo.getRecord`); 71 80 url.searchParams.set("repo", did); ··· 82 91 success: false, 83 92 error: new Error( 84 93 error.message || 85 - `Failed to fetch ${entityName}: ${response.statusText}`, 94 + `Failed to fetch ${collection}: ${response.statusText}`, 86 95 ), 87 96 }; 88 97 } ··· 118 127 119 128 async function createRecord<TSchema extends BaseSchema>( 120 129 agent: OAuthUserAgent, 121 - record: InferOutput<TSchema> & { $type: Collection }, 130 + record: InferOutput<TSchema>, 122 131 schema: TSchema, 123 132 ): Promise<Result<{ uri: AtUri; cid: string; rkey: Rkey }>> { 133 + const collection = getCollectionFromSchema(schema); 124 134 try { 125 135 const validation = safeParse(schema, record); 126 136 if (!validation.ok) { 127 137 return { 128 138 success: false, 129 - error: new Error( 130 - `Invalid ${record.$type} record: ${validation.message}`, 131 - ), 139 + error: new Error(`Invalid ${collection} record: ${validation.message}`), 132 140 }; 133 141 } 134 142 ··· 136 144 const response = await client.post("com.atproto.repo.createRecord", { 137 145 input: { 138 146 repo: agent.sub, 139 - collection: record.$type, 140 - record, 147 + collection, 148 + record: record as Record<string, unknown>, 141 149 }, 142 150 }); 143 151 ··· 145 153 return { 146 154 success: false, 147 155 error: new Error( 148 - response.data.message || `Failed to create ${record.$type} record`, 156 + response.data.message || `Failed to create ${collection} record`, 149 157 ), 150 158 }; 151 159 } ··· 175 183 async function updateRecord<TSchema extends BaseSchema>( 176 184 agent: OAuthUserAgent, 177 185 rkey: Rkey, 178 - record: InferOutput<TSchema> & { $type: Collection }, 186 + record: InferOutput<TSchema>, 179 187 schema: TSchema, 180 188 ): Promise<Result<{ uri: AtUri; cid: string }>> { 189 + const collection = getCollectionFromSchema(schema); 181 190 try { 182 191 const validation = safeParse(schema, record); 183 192 if (!validation.ok) { 184 193 return { 185 194 success: false, 186 - error: new Error( 187 - `Invalid ${record.$type} record: ${validation.message}`, 188 - ), 195 + error: new Error(`Invalid ${collection} record: ${validation.message}`), 189 196 }; 190 197 } 191 198 ··· 193 200 const response = await client.post("com.atproto.repo.putRecord", { 194 201 input: { 195 202 repo: agent.sub, 196 - collection: record.$type, 203 + collection, 197 204 rkey, 198 - record, 205 + record: record as Record<string, unknown>, 199 206 }, 200 207 }); 201 208 ··· 203 210 return { 204 211 success: false, 205 212 error: new Error( 206 - response.data.message || `Failed to update ${record.$type} record`, 213 + response.data.message || `Failed to update ${collection} record`, 207 214 ), 208 215 }; 209 216 } ··· 226 233 async function listRecords<TSchema extends BaseSchema>( 227 234 pdsUrl: PdsUrl, 228 235 did: Did, 229 - collection: Collection, 230 - entityName: string, 231 236 schema: TSchema, 232 237 cursor?: string, 233 238 ): Promise<Result<ListRecordsResponse<InferOutput<TSchema>>>> { 239 + const collection = getCollectionFromSchema(schema); 234 240 try { 235 241 const url = new URL(`${pdsUrl}/xrpc/com.atproto.repo.listRecords`); 236 242 url.searchParams.set("repo", did); ··· 249 255 success: false, 250 256 error: new Error( 251 257 error.message || 252 - `Failed to list ${entityName}s: ${response.statusText}`, 258 + `Failed to list ${collection}: ${response.statusText}`, 253 259 ), 254 260 }; 255 261 } ··· 264 270 const result = safeParse(schema, record.value); 265 271 if (!result.ok) { 266 272 console.warn( 267 - `Skipping malformed ${entityName} record ${record.uri}: ${result.message}`, 273 + `Skipping malformed ${collection} record ${record.uri}: ${result.message}`, 268 274 ); 269 275 continue; 270 276 } ··· 287 293 } 288 294 } 289 295 290 - async function deleteRecord( 296 + async function deleteRecord<TSchema extends BaseSchema>( 291 297 agent: OAuthUserAgent, 292 298 rkey: Rkey, 293 - collection: Collection, 294 - entityName: string, 299 + schema: TSchema, 295 300 ): Promise<Result<void>> { 301 + const collection = getCollectionFromSchema(schema); 296 302 try { 297 303 const client = new Client({ handler: agent }); 298 304 const response = await client.post("com.atproto.repo.deleteRecord", { ··· 307 313 return { 308 314 success: false, 309 315 error: new Error( 310 - response.data.message || `Failed to delete ${entityName} record`, 316 + response.data.message || `Failed to delete ${collection} record`, 311 317 ), 312 318 }; 313 319 } ··· 328 334 export type DeckRecordResponse = RecordResponse<ComDeckbelcherDeckList.Main>; 329 335 330 336 export function getDeckRecord(did: Did, rkey: Rkey) { 331 - return getRecord( 332 - did, 333 - rkey, 334 - DECK_COLLECTION, 335 - "deck", 336 - ComDeckbelcherDeckList.mainSchema, 337 - ); 337 + return getRecord(did, rkey, ComDeckbelcherDeckList.mainSchema); 338 338 } 339 339 340 340 export function createDeckRecord( ··· 353 353 } 354 354 355 355 export function listUserDecks(pdsUrl: PdsUrl, did: Did, cursor?: string) { 356 - return listRecords( 357 - pdsUrl, 358 - did, 359 - DECK_COLLECTION, 360 - "deck", 361 - ComDeckbelcherDeckList.mainSchema, 362 - cursor, 363 - ); 356 + return listRecords(pdsUrl, did, ComDeckbelcherDeckList.mainSchema, cursor); 364 357 } 365 358 366 359 export function deleteDeckRecord(agent: OAuthUserAgent, rkey: Rkey) { 367 - return deleteRecord(agent, rkey, DECK_COLLECTION, "deck"); 360 + return deleteRecord(agent, rkey, ComDeckbelcherDeckList.mainSchema); 368 361 } 369 362 370 363 // ============================================================================ ··· 375 368 RecordResponse<ComDeckbelcherCollectionList.Main>; 376 369 377 370 export function getCollectionListRecord(did: Did, rkey: Rkey) { 378 - return getRecord( 379 - did, 380 - rkey, 381 - LIST_COLLECTION, 382 - "list", 383 - ComDeckbelcherCollectionList.mainSchema, 384 - ); 371 + return getRecord(did, rkey, ComDeckbelcherCollectionList.mainSchema); 385 372 } 386 373 387 374 export function createCollectionListRecord( ··· 412 399 return listRecords( 413 400 pdsUrl, 414 401 did, 415 - LIST_COLLECTION, 416 - "list", 417 402 ComDeckbelcherCollectionList.mainSchema, 418 403 cursor, 419 404 ); 420 405 } 421 406 422 407 export function deleteCollectionListRecord(agent: OAuthUserAgent, rkey: Rkey) { 423 - return deleteRecord(agent, rkey, LIST_COLLECTION, "list"); 408 + return deleteRecord(agent, rkey, ComDeckbelcherCollectionList.mainSchema); 424 409 }