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