Highly ambitious ATProtocol AppView service and sdks
1# SDK Usage Guide 2 3This guide covers how to use the generated TypeScript SDK for your slice. 4 5## Installation 6 7After generating your TypeScript client, you can use it directly in your 8project: 9 10```typescript 11import { AtprotoClient } from "./generated_client.ts"; 12import { OAuthClient } from "@slices/oauth"; 13``` 14 15## Basic Setup 16 17### Without Authentication (Read-Only) 18 19```typescript 20const client = new AtprotoClient({ 21 baseUrl: "https://api.slices.network", 22 sliceUri: "at://did:plc:abc/network.slices.slice/your-slice-rkey", 23}); 24 25// Read operations work without auth 26const albums = await client.com.recordcollector.album.getRecords(); 27``` 28 29### With Authentication (Full Access) 30 31```typescript 32import { OAuthClient } from "@slices/oauth"; 33 34// Set up OAuth client 35const oauthClient = new OAuthClient({ 36 clientId: "your-client-id", 37 clientSecret: "your-client-secret", 38 authBaseUrl: "https://aip.your-domain.com", 39 redirectUri: "https://your-app.com/oauth/callback", 40 scopes: ["atproto", "transition:generic"], 41}); 42 43// Initialize API client with OAuth 44const client = new AtprotoClient({ 45 baseUrl: "https://api.slices.network", 46 sliceUri: "at://did:plc:abc/network.slices.slice/your-slice-rkey", 47 auth: oauthClient, 48}); 49``` 50 51## CRUD Operations 52 53### Getting Records 54 55The SDK uses `getRecords` for retrieving records: 56 57```typescript 58// Get all vinyl records 59const albums = await client.com.recordcollector.album.getRecords(); 60 61// With pagination 62const page1 = await client.com.recordcollector.album.getRecords({ limit: 20 }); 63const page2 = await client.com.recordcollector.album.getRecords({ 64 limit: 20, 65 cursor: page1.cursor, 66}); 67 68// With filtering using where clause 69const nirvanaAlbums = await client.com.recordcollector.album.getRecords({ 70 where: { 71 artist: { eq: "Nirvana" }, 72 }, 73}); 74 75// Text search in specific fields 76const searchResults = await client.com.recordcollector.album.getRecords({ 77 where: { 78 title: { contains: "nevermind" }, 79 }, 80}); 81 82// Global text search across ALL fields using 'json' 83const globalSearch = await client.com.recordcollector.album.getRecords({ 84 where: { 85 json: { contains: "grunge" }, 86 }, 87}); 88 89// Combine multiple filters 90const seattleGrunge = await client.com.recordcollector.album.getRecords({ 91 where: { 92 city: { eq: "Seattle" }, 93 genre: { contains: "grunge" }, 94 }, 95 limit: 50, 96}); 97 98// Advanced filtering with multiple conditions 99const complexFilter = await client.com.recordcollector.album.getRecords({ 100 where: { 101 artist: { contains: "alice" }, 102 releaseDate: { gte: "1990-01-01" }, 103 condition: { in: ["Mint", "Near Mint"] }, 104 }, 105 limit: 25, 106}); 107 108// Filtering with exact matches 109const exactMatch = await client.com.recordcollector.album.getRecords({ 110 where: { 111 artist: { eq: "Soundgarden" }, 112 genre: { contains: "grunge" }, 113 }, 114}); 115 116// Date range filtering 117const nineties = await client.com.recordcollector.album.getRecords({ 118 where: { 119 releaseDate: { 120 gte: "1990-01-01", 121 lte: "1999-12-31", 122 }, 123 }, 124}); 125 126// With sorting 127const recentAlbums = await client.com.recordcollector.album.getRecords({ 128 sortBy: [{ field: "releaseDate", direction: "desc" }], 129}); 130 131// Multiple sort fields 132const sortedAlbums = await client.com.recordcollector.album.getRecords({ 133 sortBy: [ 134 { field: "releaseDate", direction: "desc" }, 135 { field: "title", direction: "asc" }, 136 ], 137}); 138``` 139 140### Counting Records 141 142The `countRecords` method allows you to count records without fetching them, 143using the same filtering parameters as `getRecords`: 144 145```typescript 146// Count all records 147const total = await client.com.recordcollector.album.countRecords(); 148console.log(`Total albums: ${total.count}`); 149 150// Count with filtering 151const nirvanaCount = await client.com.recordcollector.album.countRecords({ 152 where: { 153 artist: { eq: "Nirvana" }, 154 }, 155}); 156 157// Count with text search 158const searchCount = await client.com.recordcollector.album.countRecords({ 159 where: { 160 title: { contains: "nevermind" }, 161 }, 162}); 163 164// Count with multiple filters 165const filteredCount = await client.com.recordcollector.album.countRecords({ 166 where: { 167 artist: { eq: "Alice in Chains" }, 168 json: { contains: "grunge" }, 169 }, 170}); 171 172// Count with OR conditions 173const orCount = await client.com.recordcollector.album.countRecords({ 174 where: { 175 releaseDate: { eq: "1991-09-24" }, 176 }, 177 orWhere: { 178 title: { contains: "nevermind" }, 179 artist: { eq: "Pearl Jam" }, 180 }, 181}); 182 183console.log(`Found ${filteredCount.count} matching albums`); 184console.log(`Found ${orCount.count} albums with OR conditions`); 185``` 186 187### Getting a Single Record 188 189```typescript 190const album = await client.com.recordcollector.album.getRecord({ 191 uri: "at://did:plc:abc/com.recordcollector.album/3jklmno456", 192}); 193 194console.log(album.value.title); 195console.log(album.value.artist); 196``` 197 198### Creating Records 199 200```typescript 201// Create with auto-generated key 202const newAlbum = await client.com.recordcollector.album.createRecord({ 203 title: "In Utero", 204 artist: "Nirvana", 205 releaseDate: "1993-09-21", 206 genre: ["grunge", "alternative rock"], 207}); 208 209console.log(`Created: ${newAlbum.uri}`); 210 211// Create with custom key 212const customAlbum = await client.com.recordcollector.album.createRecord( 213 { 214 title: "Badmotorfinger", 215 artist: "Soundgarden", 216 releaseDate: "1991-10-08", 217 }, 218 true, // useSelfRkey for singleton records like profiles 219); 220``` 221 222### Updating Records 223 224```typescript 225// Get the record key from the URI 226const uri = "at://did:plc:abc/com.recordcollector.album/3jklmno456"; 227const rkey = uri.split("/").pop(); // '3jklmno456' 228 229const updated = await client.com.recordcollector.album.updateRecord( 230 rkey, 231 { 232 title: "Nevermind (Remastered)", 233 artist: "Nirvana", 234 releaseDate: "1991-09-24", 235 updatedAt: new Date().toISOString(), 236 }, 237); 238 239console.log(`Updated: ${updated.cid}`); 240``` 241 242### Deleting Records 243 244```typescript 245const rkey = "3jklmno456"; 246await client.com.recordcollector.album.deleteRecord(rkey); 247``` 248 249## Working with External Collections 250 251Access synced external collections like Bluesky profiles: 252 253```typescript 254// Get Bluesky profiles in your slice 255const profiles = await client.app.bsky.actor.profile.getRecords(); 256 257// Get a specific profile 258const profile = await client.app.bsky.actor.profile.getRecord({ 259 uri: "at://did:plc:user/app.bsky.actor.profile/self", 260}); 261 262// Access profile data 263console.log(profile.value.displayName); 264console.log(profile.value.description); 265``` 266 267## Blob Handling 268 269### Uploading Blobs 270 271```typescript 272// Read file as ArrayBuffer 273const file = await Deno.readFile("./nevermind-cover.jpg"); 274 275// Upload blob 276const blobResponse = await client.uploadBlob({ 277 data: file, 278 mimeType: "image/jpeg", 279}); 280 281// Use blob in a record 282const albumWithArt = await client.com.recordcollector.album.createRecord({ 283 title: "Nevermind", 284 artist: "Nirvana", 285 releaseDate: "1991-09-24", 286 genre: ["grunge", "alternative rock"], 287 condition: "Near Mint", 288 albumArt: blobResponse.blob, 289}); 290``` 291 292### Converting Blobs to CDN URLs 293 294```typescript 295import { recordBlobToCdnUrl } from "./generated-client.ts"; 296 297// Get a record with a blob 298const profile = await client.app.bsky.actor.profile.getRecord({ 299 uri: "at://did:plc:user/app.bsky.actor.profile/self", 300}); 301 302// Convert avatar blob to CDN URL 303if (profile.value.avatar) { 304 const avatarUrl = recordBlobToCdnUrl( 305 profile, 306 profile.value.avatar, 307 "avatar", // Size preset 308 ); 309 console.log(`Avatar URL: ${avatarUrl}`); 310} 311 312// Available presets: 313// - 'avatar': Small square images 314// - 'banner': Wide header images 315// - 'feed_thumbnail': Small feed previews 316// - 'feed_fullsize': Full resolution images 317``` 318 319## Slice Operations 320 321### Get Actors 322 323The `getActors` method retrieves actors (users) within a slice with powerful 324filtering and sorting capabilities: 325 326```typescript 327// Get all actors in the slice 328const actors = await client.getActors(); 329 330// With pagination 331const page1 = await client.getActors({ 332 limit: 20, 333}); 334const page2 = await client.getActors({ 335 limit: 20, 336 cursor: page1.cursor, 337}); 338 339// Filter by specific DIDs 340const specificActors = await client.getActors({ 341 where: { 342 did: { in: ["did:plc:user1", "did:plc:user2"] }, 343 }, 344}); 345 346// Search by handle 347const searchByHandle = await client.getActors({ 348 where: { 349 handle: { contains: "alice" }, 350 }, 351}); 352 353// Filter by exact handle 354const exactHandle = await client.getActors({ 355 where: { 356 handle: { eq: "user.bsky.social" }, 357 }, 358}); 359 360// Available where fields for actors: 361// - did: Filter by decentralized identifier 362// - handle: Filter by handle/username 363// - indexed_at: Filter by when the actor was indexed 364``` 365 366### Browse Slice Records 367 368The `getSliceRecords` method retrieves records across multiple collections: 369 370```typescript 371// Get records from specific collections 372const records = await client.getSliceRecords({ 373 where: { 374 collection: { eq: "com.recordcollector.album" }, 375 did: { eq: "did:plc:specific-author" }, // optional 376 }, 377 limit: 50, 378}); 379 380records.records.forEach((record) => { 381 console.log(`${record.uri}: ${JSON.stringify(record.value)}`); 382}); 383 384// Search across collections using specific fields 385const searchResults = await client.getSliceRecords({ 386 where: { 387 collection: { eq: "com.recordcollector.album" }, 388 title: { contains: "nevermind" }, 389 did: { eq: "did:plc:specific-author" }, // optional 390 }, 391 limit: 50, 392}); 393 394// Global search across ALL fields in records 395const globalSearchResults = await client.getSliceRecords({ 396 where: { 397 collection: { eq: "com.recordcollector.album" }, 398 json: { contains: "grunge" }, // Searches entire record content 399 did: { eq: "did:plc:specific-author" }, // optional 400 }, 401 limit: 50, 402}); 403 404searchResults.records.forEach((record) => { 405 console.log(`Found: ${record.uri} - ${JSON.stringify(record.value)}`); 406}); 407 408// Get records from any collection with global text search 409const allCollectionSearch = await client.getSliceRecords({ 410 where: { 411 json: { contains: "seattle" }, // Searches ALL fields in ALL collections 412 }, 413 limit: 20, 414}); 415``` 416 417## Search Capabilities 418 419The SDK provides flexible search options for finding records: 420 421### Field-Specific Search 422 423Search within specific fields of your records: 424 425```typescript 426// Search in title field only 427const titleSearch = await client.com.recordcollector.album.getRecords({ 428 where: { 429 title: { contains: "nevermind" }, 430 }, 431}); 432 433// Search in notes field 434const notesSearch = await client.com.recordcollector.album.getRecords({ 435 where: { 436 notes: { contains: "original pressing" }, 437 }, 438}); 439``` 440 441### Global JSON Search 442 443Use the special `json` field to search across **all fields** in a record: 444 445```typescript 446// Finds records containing "grunge" anywhere in their data 447const globalSearch = await client.com.recordcollector.album.getRecords({ 448 where: { 449 json: { contains: "grunge" }, 450 }, 451}); 452 453// This will match records where "grunge" appears in: 454// - title: "Nevermind" 455// - artist: "Nirvana" 456// - genre: ["grunge", "alternative rock"] 457// - notes: "Classic grunge album from Seattle" 458// - or any other field in the record 459``` 460 461### Cross-Collection Search 462 463When using `getSliceRecords`, you can search across multiple collections: 464 465```typescript 466// Search for "seattle" across all collections 467const crossCollectionSearch = await client.getSliceRecords({ 468 where: { 469 json: { contains: "seattle" }, 470 }, 471}); 472 473// Limit to specific collections 474const specificSearch = await client.getSliceRecords({ 475 where: { 476 collection: { 477 in: ["com.recordcollector.album", "com.recordcollector.review"], 478 }, 479 json: { contains: "grunge" }, 480 }, 481}); 482``` 483 484### OR Query Support 485 486You can use OR queries to find records that match any of multiple conditions 487using the separate `orWhere` parameter. This provides clean type safety and 488autocomplete for field names: 489 490```typescript 491// Find albums by either Nirvana OR Alice in Chains 492const albums = await client.com.recordcollector.album.getRecords({ 493 orWhere: { 494 artist: { in: ["Nirvana", "Alice in Chains"] }, 495 }, 496}); 497 498// Find albums that either have "nevermind" in title OR are by Soundgarden 499const albums = await client.com.recordcollector.album.getRecords({ 500 orWhere: { 501 title: { contains: "nevermind" }, 502 artist: { eq: "Soundgarden" }, 503 }, 504}); 505 506// Combining OR with regular AND conditions 507const albums = await client.com.recordcollector.album.getRecords({ 508 where: { 509 releaseDate: { eq: "1991-09-24" }, // AND conditions 510 }, 511 orWhere: { // OR conditions 512 artist: { contains: "nirvana" }, 513 genre: { contains: "grunge" }, 514 }, 515}); 516// SQL: WHERE release_date = '1991-09-24' AND (artist LIKE '%nirvana%' OR genre LIKE '%grunge%') 517 518// OR queries work with cross-collection searches too 519const crossCollectionOrSearch = await client.getSliceRecords({ 520 where: { 521 collection: { eq: "com.recordcollector.album" }, 522 }, 523 orWhere: { 524 artist: { contains: "pearl jam" }, 525 genre: { contains: "alternative rock" }, 526 }, 527}); 528 529// You get full autocomplete and type safety for field names in both where and orWhere 530const typedSearch = await client.com.recordcollector.album.getRecords({ 531 where: { 532 // TypeScript autocompletes valid field names here 533 condition: { contains: "mint" }, 534 }, 535 orWhere: { 536 // And also provides autocomplete here 537 artist: { contains: "soundgarden" }, 538 genre: { contains: "grunge" }, 539 }, 540}); 541``` 542 543### Sync User Collections 544 545```typescript 546// Sync current user's data (requires auth) 547const syncResult = await client.syncUserCollections(); 548 549console.log(`Synced ${syncResult.recordsSynced} records`); 550``` 551 552## Error Handling 553 554```typescript 555try { 556 const post = await client.com.example.post.getRecord({ 557 uri: "at://invalid-uri", 558 }); 559} catch (error) { 560 if (error.message.includes("404")) { 561 console.log("Record not found"); 562 } else if (error.message.includes("401")) { 563 console.log("Authentication required"); 564 } else { 565 console.error("Unexpected error:", error); 566 } 567} 568``` 569 570## OAuth Authentication Flow 571 572### 1. Initialize OAuth 573 574```typescript 575const oauthClient = new OAuthClient({ 576 clientId: process.env.OAUTH_CLIENT_ID, 577 clientSecret: process.env.OAUTH_CLIENT_SECRET, 578 authBaseUrl: process.env.OAUTH_AIP_BASE_URL, 579 redirectUri: "https://your-app.com/oauth/callback", 580}); 581``` 582 583### 2. Start Authorization 584 585```typescript 586const authResult = await oauthClient.authorize({ 587 loginHint: "user.bsky.social", 588}); 589 590// Redirect user to authorization URL 591window.location.href = authResult.authorizationUrl; 592``` 593 594### 3. Handle Callback 595 596```typescript 597// In your callback handler 598const urlParams = new URLSearchParams(window.location.search); 599const code = urlParams.get("code"); 600const state = urlParams.get("state"); 601 602await oauthClient.handleCallback({ code, state }); 603``` 604 605### 4. Use Authenticated Client 606 607```typescript 608const client = new AtprotoClient({ 609 baseUrl: "https://api.slices.network", 610 sliceUri: "at://did:plc:abc/network.slices.slice/your-slice-rkey", 611 auth: oauthClient, 612}); 613 614// OAuth tokens are automatically managed 615const album = await client.com.recordcollector.album.createRecord({ 616 title: "Ten", 617 artist: "Pearl Jam", 618 releaseDate: "1991-08-27", 619 genre: ["grunge", "alternative rock"], 620 condition: "Mint", 621}); 622``` 623 624## Type Safety 625 626The generated SDK provides full TypeScript type safety: 627 628```typescript 629// TypeScript knows the shape of your records 630const album = await client.com.recordcollector.album.getRecord({ uri }); 631 632// Type error: property 'unknownField' does not exist 633// album.value.unknownField 634 635// Autocomplete works for all fields 636album.value.title; // string 637album.value.artist; // string 638album.value.genre; // string[] 639album.value.releaseDate; // string 640 641// Creating records is type-checked 642await client.com.recordcollector.album.createRecord({ 643 title: "Dirt", 644 artist: "Alice in Chains", 645 releaseDate: "1992-09-29", 646 genre: ["grunge", "alternative metal"], 647 condition: "Very Good Plus", 648 // Type error: 'invalidField' is not assignable 649 // invalidField: "This will error" 650}); 651``` 652 653## Advanced Patterns 654 655### Batch Operations 656 657```typescript 658// Process records in batches 659 660async function* getAllAlbums() { 661 let cursor: string | undefined; 662 663 do { 664 const batch = await client.com.recordcollector.album.getRecords({ 665 limit: 100, 666 cursor, 667 }); 668 669 yield* batch.records; 670 cursor = batch.cursor; 671 } while (cursor); 672} 673 674// Use the generator 675for await (const album of getAllAlbums()) { 676 console.log(`${album.value.artist} - ${album.value.title}`); 677} 678``` 679 680## Next Steps 681 682- [API Reference](./api-reference.md) - Complete endpoint documentation 683- [Concepts](./concepts.md) - Understand the architecture 684- [Getting Started](./getting-started.md) - Initial setup guide