Highly ambitious ATProtocol AppView service and sdks

SDK Usage Guide#

This guide covers how to use the generated TypeScript SDK for your slice.

Installation#

After generating your TypeScript client, you can use it directly in your project:

import { AtprotoClient } from "./generated_client.ts";
import { OAuthClient } from "@slices/oauth";

Basic Setup#

Without Authentication (Read-Only)#

const client = new AtprotoClient({
  baseUrl: "https://api.slices.network",
  sliceUri: "at://did:plc:abc/network.slices.slice/your-slice-rkey",
});

// Read operations work without auth
const albums = await client.com.recordcollector.album.getRecords();

With Authentication (Full Access)#

import { OAuthClient } from "@slices/oauth";

// Set up OAuth client
const oauthClient = new OAuthClient({
  clientId: "your-client-id",
  clientSecret: "your-client-secret",
  authBaseUrl: "https://aip.your-domain.com",
  redirectUri: "https://your-app.com/oauth/callback",
  scopes: ["atproto", "transition:generic"],
});

// Initialize API client with OAuth
const client = new AtprotoClient({
  baseUrl: "https://api.slices.network",
  sliceUri: "at://did:plc:abc/network.slices.slice/your-slice-rkey",
  auth: oauthClient,
});

CRUD Operations#

Getting Records#

The SDK uses getRecords for retrieving records:

// Get all vinyl records
const albums = await client.com.recordcollector.album.getRecords();

// With pagination
const page1 = await client.com.recordcollector.album.getRecords({ limit: 20 });
const page2 = await client.com.recordcollector.album.getRecords({
  limit: 20,
  cursor: page1.cursor,
});

// With filtering using where clause
const nirvanaAlbums = await client.com.recordcollector.album.getRecords({
  where: {
    artist: { eq: "Nirvana" },
  },
});

// Text search in specific fields
const searchResults = await client.com.recordcollector.album.getRecords({
  where: {
    title: { contains: "nevermind" },
  },
});

// Global text search across ALL fields using 'json'
const globalSearch = await client.com.recordcollector.album.getRecords({
  where: {
    json: { contains: "grunge" },
  },
});

// Combine multiple filters
const seattleGrunge = await client.com.recordcollector.album.getRecords({
  where: {
    city: { eq: "Seattle" },
    genre: { contains: "grunge" },
  },
  limit: 50,
});

// Advanced filtering with multiple conditions
const complexFilter = await client.com.recordcollector.album.getRecords({
  where: {
    artist: { contains: "alice" },
    releaseDate: { gte: "1990-01-01" },
    condition: { in: ["Mint", "Near Mint"] },
  },
  limit: 25,
});

// Filtering with exact matches
const exactMatch = await client.com.recordcollector.album.getRecords({
  where: {
    artist: { eq: "Soundgarden" },
    genre: { contains: "grunge" },
  },
});

// Date range filtering
const nineties = await client.com.recordcollector.album.getRecords({
  where: {
    releaseDate: {
      gte: "1990-01-01",
      lte: "1999-12-31",
    },
  },
});

// With sorting
const recentAlbums = await client.com.recordcollector.album.getRecords({
  sortBy: [{ field: "releaseDate", direction: "desc" }],
});

// Multiple sort fields
const sortedAlbums = await client.com.recordcollector.album.getRecords({
  sortBy: [
    { field: "releaseDate", direction: "desc" },
    { field: "title", direction: "asc" },
  ],
});

Counting Records#

The countRecords method allows you to count records without fetching them, using the same filtering parameters as getRecords:

// Count all records
const total = await client.com.recordcollector.album.countRecords();
console.log(`Total albums: ${total.count}`);

// Count with filtering
const nirvanaCount = await client.com.recordcollector.album.countRecords({
  where: {
    artist: { eq: "Nirvana" },
  },
});

// Count with text search
const searchCount = await client.com.recordcollector.album.countRecords({
  where: {
    title: { contains: "nevermind" },
  },
});

// Count with multiple filters
const filteredCount = await client.com.recordcollector.album.countRecords({
  where: {
    artist: { eq: "Alice in Chains" },
    json: { contains: "grunge" },
  },
});

// Count with OR conditions
const orCount = await client.com.recordcollector.album.countRecords({
  where: {
    releaseDate: { eq: "1991-09-24" },
  },
  orWhere: {
    title: { contains: "nevermind" },
    artist: { eq: "Pearl Jam" },
  },
});

console.log(`Found ${filteredCount.count} matching albums`);
console.log(`Found ${orCount.count} albums with OR conditions`);

Getting a Single Record#

const album = await client.com.recordcollector.album.getRecord({
  uri: "at://did:plc:abc/com.recordcollector.album/3jklmno456",
});

console.log(album.value.title);
console.log(album.value.artist);

Creating Records#

// Create with auto-generated key
const newAlbum = await client.com.recordcollector.album.createRecord({
  title: "In Utero",
  artist: "Nirvana",
  releaseDate: "1993-09-21",
  genre: ["grunge", "alternative rock"],
});

console.log(`Created: ${newAlbum.uri}`);

// Create with custom key
const customAlbum = await client.com.recordcollector.album.createRecord(
  {
    title: "Badmotorfinger",
    artist: "Soundgarden",
    releaseDate: "1991-10-08",
  },
  true, // useSelfRkey for singleton records like profiles
);

Updating Records#

// Get the record key from the URI
const uri = "at://did:plc:abc/com.recordcollector.album/3jklmno456";
const rkey = uri.split("/").pop(); // '3jklmno456'

const updated = await client.com.recordcollector.album.updateRecord(
  rkey,
  {
    title: "Nevermind (Remastered)",
    artist: "Nirvana",
    releaseDate: "1991-09-24",
    updatedAt: new Date().toISOString(),
  },
);

console.log(`Updated: ${updated.cid}`);

Deleting Records#

const rkey = "3jklmno456";
await client.com.recordcollector.album.deleteRecord(rkey);

Working with External Collections#

Access synced external collections like Bluesky profiles:

// Get Bluesky profiles in your slice
const profiles = await client.app.bsky.actor.profile.getRecords();

// Get a specific profile
const profile = await client.app.bsky.actor.profile.getRecord({
  uri: "at://did:plc:user/app.bsky.actor.profile/self",
});

// Access profile data
console.log(profile.value.displayName);
console.log(profile.value.description);

Blob Handling#

Uploading Blobs#

// Read file as ArrayBuffer
const file = await Deno.readFile("./nevermind-cover.jpg");

// Upload blob
const blobResponse = await client.uploadBlob({
  data: file,
  mimeType: "image/jpeg",
});

// Use blob in a record
const albumWithArt = await client.com.recordcollector.album.createRecord({
  title: "Nevermind",
  artist: "Nirvana",
  releaseDate: "1991-09-24",
  genre: ["grunge", "alternative rock"],
  condition: "Near Mint",
  albumArt: blobResponse.blob,
});

Converting Blobs to CDN URLs#

import { recordBlobToCdnUrl } from "./generated-client.ts";

// Get a record with a blob
const profile = await client.app.bsky.actor.profile.getRecord({
  uri: "at://did:plc:user/app.bsky.actor.profile/self",
});

// Convert avatar blob to CDN URL
if (profile.value.avatar) {
  const avatarUrl = recordBlobToCdnUrl(
    profile,
    profile.value.avatar,
    "avatar", // Size preset
  );
  console.log(`Avatar URL: ${avatarUrl}`);
}

// Available presets:
// - 'avatar': Small square images
// - 'banner': Wide header images
// - 'feed_thumbnail': Small feed previews
// - 'feed_fullsize': Full resolution images

Slice Operations#

Get Actors#

The getActors method retrieves actors (users) within a slice with powerful filtering and sorting capabilities:

// Get all actors in the slice
const actors = await client.getActors();

// With pagination
const page1 = await client.getActors({
  limit: 20,
});
const page2 = await client.getActors({
  limit: 20,
  cursor: page1.cursor,
});

// Filter by specific DIDs
const specificActors = await client.getActors({
  where: {
    did: { in: ["did:plc:user1", "did:plc:user2"] },
  },
});

// Search by handle
const searchByHandle = await client.getActors({
  where: {
    handle: { contains: "alice" },
  },
});

// Filter by exact handle
const exactHandle = await client.getActors({
  where: {
    handle: { eq: "user.bsky.social" },
  },
});

// Available where fields for actors:
// - did: Filter by decentralized identifier
// - handle: Filter by handle/username
// - indexed_at: Filter by when the actor was indexed

Browse Slice Records#

The getSliceRecords method retrieves records across multiple collections:

// Get records from specific collections
const records = await client.getSliceRecords({
  where: {
    collection: { eq: "com.recordcollector.album" },
    did: { eq: "did:plc:specific-author" }, // optional
  },
  limit: 50,
});

records.records.forEach((record) => {
  console.log(`${record.uri}: ${JSON.stringify(record.value)}`);
});

// Search across collections using specific fields
const searchResults = await client.getSliceRecords({
  where: {
    collection: { eq: "com.recordcollector.album" },
    title: { contains: "nevermind" },
    did: { eq: "did:plc:specific-author" }, // optional
  },
  limit: 50,
});

// Global search across ALL fields in records
const globalSearchResults = await client.getSliceRecords({
  where: {
    collection: { eq: "com.recordcollector.album" },
    json: { contains: "grunge" }, // Searches entire record content
    did: { eq: "did:plc:specific-author" }, // optional
  },
  limit: 50,
});

searchResults.records.forEach((record) => {
  console.log(`Found: ${record.uri} - ${JSON.stringify(record.value)}`);
});

// Get records from any collection with global text search
const allCollectionSearch = await client.getSliceRecords({
  where: {
    json: { contains: "seattle" }, // Searches ALL fields in ALL collections
  },
  limit: 20,
});

Search Capabilities#

The SDK provides flexible search options for finding records:

Search within specific fields of your records:

// Search in title field only
const titleSearch = await client.com.recordcollector.album.getRecords({
  where: {
    title: { contains: "nevermind" },
  },
});

// Search in notes field
const notesSearch = await client.com.recordcollector.album.getRecords({
  where: {
    notes: { contains: "original pressing" },
  },
});

Use the special json field to search across all fields in a record:

// Finds records containing "grunge" anywhere in their data
const globalSearch = await client.com.recordcollector.album.getRecords({
  where: {
    json: { contains: "grunge" },
  },
});

// This will match records where "grunge" appears in:
// - title: "Nevermind"
// - artist: "Nirvana"
// - genre: ["grunge", "alternative rock"]
// - notes: "Classic grunge album from Seattle"
// - or any other field in the record

When using getSliceRecords, you can search across multiple collections:

// Search for "seattle" across all collections
const crossCollectionSearch = await client.getSliceRecords({
  where: {
    json: { contains: "seattle" },
  },
});

// Limit to specific collections
const specificSearch = await client.getSliceRecords({
  where: {
    collection: {
      in: ["com.recordcollector.album", "com.recordcollector.review"],
    },
    json: { contains: "grunge" },
  },
});

OR Query Support#

You can use OR queries to find records that match any of multiple conditions using the separate orWhere parameter. This provides clean type safety and autocomplete for field names:

// Find albums by either Nirvana OR Alice in Chains
const albums = await client.com.recordcollector.album.getRecords({
  orWhere: {
    artist: { in: ["Nirvana", "Alice in Chains"] },
  },
});

// Find albums that either have "nevermind" in title OR are by Soundgarden
const albums = await client.com.recordcollector.album.getRecords({
  orWhere: {
    title: { contains: "nevermind" },
    artist: { eq: "Soundgarden" },
  },
});

// Combining OR with regular AND conditions
const albums = await client.com.recordcollector.album.getRecords({
  where: {
    releaseDate: { eq: "1991-09-24" }, // AND conditions
  },
  orWhere: { // OR conditions
    artist: { contains: "nirvana" },
    genre: { contains: "grunge" },
  },
});
// SQL: WHERE release_date = '1991-09-24' AND (artist LIKE '%nirvana%' OR genre LIKE '%grunge%')

// OR queries work with cross-collection searches too
const crossCollectionOrSearch = await client.getSliceRecords({
  where: {
    collection: { eq: "com.recordcollector.album" },
  },
  orWhere: {
    artist: { contains: "pearl jam" },
    genre: { contains: "alternative rock" },
  },
});

// You get full autocomplete and type safety for field names in both where and orWhere
const typedSearch = await client.com.recordcollector.album.getRecords({
  where: {
    // TypeScript autocompletes valid field names here
    condition: { contains: "mint" },
  },
  orWhere: {
    // And also provides autocomplete here
    artist: { contains: "soundgarden" },
    genre: { contains: "grunge" },
  },
});

Sync User Collections#

// Sync current user's data (requires auth)
const syncResult = await client.syncUserCollections();

console.log(`Synced ${syncResult.recordsSynced} records`);

Error Handling#

try {
  const post = await client.com.example.post.getRecord({
    uri: "at://invalid-uri",
  });
} catch (error) {
  if (error.message.includes("404")) {
    console.log("Record not found");
  } else if (error.message.includes("401")) {
    console.log("Authentication required");
  } else {
    console.error("Unexpected error:", error);
  }
}

OAuth Authentication Flow#

1. Initialize OAuth#

const oauthClient = new OAuthClient({
  clientId: process.env.OAUTH_CLIENT_ID,
  clientSecret: process.env.OAUTH_CLIENT_SECRET,
  authBaseUrl: process.env.OAUTH_AIP_BASE_URL,
  redirectUri: "https://your-app.com/oauth/callback",
});

2. Start Authorization#

const authResult = await oauthClient.authorize({
  loginHint: "user.bsky.social",
});

// Redirect user to authorization URL
window.location.href = authResult.authorizationUrl;

3. Handle Callback#

// In your callback handler
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
const state = urlParams.get("state");

await oauthClient.handleCallback({ code, state });

4. Use Authenticated Client#

const client = new AtprotoClient({
  baseUrl: "https://api.slices.network",
  sliceUri: "at://did:plc:abc/network.slices.slice/your-slice-rkey",
  auth: oauthClient,
});

// OAuth tokens are automatically managed
const album = await client.com.recordcollector.album.createRecord({
  title: "Ten",
  artist: "Pearl Jam",
  releaseDate: "1991-08-27",
  genre: ["grunge", "alternative rock"],
  condition: "Mint",
});

Type Safety#

The generated SDK provides full TypeScript type safety:

// TypeScript knows the shape of your records
const album = await client.com.recordcollector.album.getRecord({ uri });

// Type error: property 'unknownField' does not exist
// album.value.unknownField

// Autocomplete works for all fields
album.value.title; // string
album.value.artist; // string
album.value.genre; // string[]
album.value.releaseDate; // string

// Creating records is type-checked
await client.com.recordcollector.album.createRecord({
  title: "Dirt",
  artist: "Alice in Chains",
  releaseDate: "1992-09-29",
  genre: ["grunge", "alternative metal"],
  condition: "Very Good Plus",
  // Type error: 'invalidField' is not assignable
  // invalidField: "This will error"
});

Advanced Patterns#

Batch Operations#

// Process records in batches

async function* getAllAlbums() {
  let cursor: string | undefined;

  do {
    const batch = await client.com.recordcollector.album.getRecords({
      limit: 100,
      cursor,
    });

    yield* batch.records;
    cursor = batch.cursor;
  } while (cursor);
}

// Use the generator
for await (const album of getAllAlbums()) {
  console.log(`${album.value.artist} - ${album.value.title}`);
}

Next Steps#