# GraphQL API Slices provides a powerful GraphQL API for querying indexed AT Protocol data. The API automatically generates schema from your lexicons and provides efficient querying with relationship traversal. ## Accessing the API GraphQL endpoints are available per-slice: ``` POST /graphql?slice= ``` ### GraphQL Playground Access the interactive GraphQL Playground in your browser: ``` https://api.slices.network/graphql?slice= ``` ## Schema Generation The GraphQL schema is automatically generated from your slice's lexicons: - **Types**: One GraphQL type per collection (e.g., `social.grain.gallery` → `SocialGrainGallery`) - **Queries**: Collection queries with filtering, sorting, and pagination - **Mutations**: Create, update, delete operations per collection - **Subscriptions**: Real-time updates for record changes ## Querying Data ### Basic Query ```graphql query { socialGrainGalleries { edges { node { uri title description createdAt } } } } ``` ### Filtering Use `where` clauses with typed filter conditions. Each collection has its own `{Collection}WhereInput` type with appropriate filters for each field. ```graphql query { socialGrainGalleries(where: { title: { contains: "Aerial" } }) { edges { node { uri title description } } } } ``` #### Filter Types The API provides three filter types based on field data types: **StringFilter** - For string fields: - `eq`: Exact match - `in`: Match any value in array - `contains`: Substring match (case-insensitive) - `fuzzy`: Fuzzy/similarity match (typo-tolerant) - `gt`: Greater than (lexicographic) - `gte`: Greater than or equal to - `lt`: Less than - `lte`: Less than or equal to **IntFilter** - For integer fields: - `eq`: Exact match - `in`: Match any value in array - `gt`: Greater than - `gte`: Greater than or equal to - `lt`: Less than - `lte`: Less than or equal to **DateTimeFilter** - For datetime fields: - `eq`: Exact match - `gt`: After datetime - `gte`: At or after datetime - `lt`: Before datetime - `lte`: At or before datetime #### Fuzzy Matching Example The `fuzzy` filter uses PostgreSQL's trigram similarity for typo-tolerant search: ```graphql query FuzzySearch { fmTealAlphaFeedPlays( where: { trackName: { fuzzy: "love" } } ) { edges { node { trackName artists } } } } ``` This will match track names like: - "Love" (exact) - "Love Song" - "Lovely" - "I Love You" - "Lover" - "Loveless" The fuzzy filter is great for: - Handling typos and misspellings - Finding similar variations of text - Flexible search without exact matching **Note**: Fuzzy matching works on the similarity between strings (using trigrams), so it's more flexible than `contains` but may return unexpected matches if the similarity threshold is met. #### Date Range Example ```graphql query RecentGalleries { socialGrainGalleries( where: { createdAt: { gte: "2025-01-01T00:00:00Z" lt: "2025-12-31T23:59:59Z" } } ) { edges { node { uri title createdAt } } } } ``` #### Multiple Conditions Combine multiple filters - they are AND'ed together: ```graphql query { socialGrainGalleries( where: { title: { contains: "Aerial" } createdAt: { gte: "2025-01-01T00:00:00Z" } } ) { edges { node { uri title description createdAt } } } } ``` #### Nested AND/OR Queries Build complex filter logic with arbitrarily nestable `and` and `or` arrays: **Simple OR - Match any condition:** ```graphql query { networkSlicesSlices( where: { or: [ { name: { contains: "grain" } } { name: { contains: "teal" } } ] } ) { edges { node { name } } } } ``` **Simple AND - Match all conditions:** ```graphql query { networkSlicesSlices( where: { and: [ { name: { contains: "grain" } } { name: { contains: "teal" } } ] } ) { edges { node { name } } } } ``` **Complex Nested Logic:** ```graphql query { appBskyFeedPost( where: { and: [ { or: [ { text: { contains: "music" } } { text: { contains: "song" } } ] } { and: [ { uri: { contains: "app.bsky" } } { uri: { contains: "post" } } ] } { createdAt: { gte: "2025-01-01T00:00:00Z" } } ] } ) { edges { node { uri text createdAt } } } } ``` This example finds posts where: - (text contains "music" OR text contains "song") AND - (uri contains "app.bsky" AND uri contains "post") AND - createdAt is after 2025-01-01 **Key Features:** - Unlimited nesting depth - `and`/`or` can be nested arbitrarily - Mix with field filters - combine nested logic with regular field conditions - Type-safe - Each collection's `WhereInput` supports `and` and `or` arrays - Available in queries and aggregations ### Pagination Relay-style cursor pagination: ```graphql query { socialGrainGalleries(first: 10, after: "cursor") { edges { cursor node { uri title } } pageInfo { hasNextPage endCursor } } } ``` ### Sorting Each collection has its own typed `{Collection}SortFieldInput` for type-safe sorting: ```graphql query { socialGrainGalleries( sortBy: [ { field: createdAt, direction: desc } ] ) { edges { node { uri title createdAt } } } } ``` **Multi-field sorting:** ```graphql query { socialGrainGalleries( sortBy: [ { field: actorHandle, direction: asc } { field: createdAt, direction: desc } ] ) { edges { node { uri title actorHandle createdAt } } } } ``` The `field` enum values are collection-specific (e.g., `SocialGrainGallerySortFieldInput`). Use GraphQL introspection or the playground to see available fields for each collection. ## Aggregations Aggregation queries allow you to group records and perform calculations. Each collection has a corresponding `{Collection}Aggregated` query. ### Basic Aggregation Group records by one or more fields and get counts: ```graphql query TopTracks { fmTealAlphaFeedPlaysAggregated( groupBy: [{ field: trackName }] orderBy: { count: desc } limit: 10 ) { trackName count } } ``` ### Multi-Field Grouping Group by multiple fields using the typed `{Collection}GroupByField` enum: ```graphql query TopTracksByArtist { fmTealAlphaFeedPlaysAggregated( groupBy: [{ field: trackName }, { field: artists }] orderBy: { count: desc } limit: 20 ) { trackName artists count } } ``` ### Filtering Aggregations Combine typed filters with aggregations for time-based analysis: ```graphql query TopTracksThisWeek { fmTealAlphaFeedPlaysAggregated( groupBy: [{ field: trackName }, { field: artists }] where: { indexedAt: { gte: "2025-01-01T00:00:00Z" lt: "2025-01-08T00:00:00Z" } trackName: { contains: "Love" } } orderBy: { count: desc } limit: 10 ) { trackName artists count } } ``` ### Aggregation Features - **Typed GroupBy**: Each collection has a `{Collection}GroupByField` enum for type-safe field selection - **Typed Filters**: Use the same `{Collection}WhereInput` as regular queries - **Sorting**: Order by `count` (ascending or descending) or any grouped field - **Pagination**: Use `limit` to control result count - **Multiple Fields**: Group by any combination of fields from your lexicon - **Date Truncation**: Group by time intervals (second, minute, hour, day, week, month, quarter, year) ### Date Truncation Group records by time intervals using the `interval` parameter in `groupBy`: ```graphql query DailyPlays { fmTealAlphaFeedPlaysAggregated( groupBy: [ { field: playedTime, interval: day } ] orderBy: { count: desc } limit: 30 ) { playedTime count } } ``` **Supported Intervals:** - `second` - Group by second - `minute` - Group by minute - `hour` - Group by hour - `day` - Group by day (common for daily reports) - `week` - Group by week (Monday-Sunday) - `month` - Group by month - `quarter` - Group by quarter (Q1-Q4) - `year` - Group by year **Combining with Regular Fields:** ```graphql query TrackPlaysByDay { fmTealAlphaFeedPlaysAggregated( groupBy: [ { field: trackName }, { field: playedTime, interval: day } ] orderBy: { count: desc } limit: 100 ) { trackName playedTime count } } ``` **How it Works:** - Uses PostgreSQL's `date_trunc()` function for efficient time bucketing - Automatically handles timestamp casting for JSON fields - Returns truncated timestamps (e.g., `2025-01-15 00:00:00` for day interval) - Works with both system fields (`indexedAt`) and lexicon datetime fields ### Use Cases **Daily/Weekly/Monthly Reports**: ```graphql query WeeklyPlays { fmTealAlphaFeedPlaysAggregated( groupBy: [{ field: trackName }] where: { playedTime: { gte: "2025-01-01T00:00:00Z" lt: "2025-01-08T00:00:00Z" } } orderBy: { count: desc } limit: 50 ) { trackName count } } ``` **Trend Analysis**: ```graphql query TrendingArtists { fmTealAlphaFeedPlaysAggregated( groupBy: [{ field: artists }] where: { playedTime: { gte: "2025-01-01T00:00:00Z" } } orderBy: { count: desc } limit: 20 ) { artists count } } ``` ## Relationships The GraphQL API automatically generates relationship fields based on your lexicon's `at-uri` fields. ### Forward Joins (References) When a record has an `at-uri` field, you get a **singular** field that resolves to the referenced record. **Lexicon Schema (social.grain.gallery.item):** ```json { "lexicon": 1, "id": "social.grain.gallery.item", "defs": { "main": { "type": "record", "key": "tid", "record": { "type": "object", "required": ["gallery", "item", "position", "createdAt"], "properties": { "gallery": { "type": "string", "format": "at-uri" }, "item": { "type": "string", "format": "at-uri" }, "position": { "type": "integer" }, "createdAt": { "type": "string", "format": "datetime" } } } } } } ``` **Generated GraphQL Type:** ```graphql type SocialGrainGalleryItem { uri: String! gallery: String! # at-uri field from lexicon item: String! # at-uri field from lexicon position: Int! createdAt: String! # Auto-generated forward joins (singular) socialGrainGallery: SocialGrainGallery socialGrainPhoto: SocialGrainPhoto } ``` **Example Query:** ```graphql query { socialGrainGalleryItems(limit: 5) { position # Follow the reference to get the photo socialGrainPhoto { uri alt aspectRatio } # Follow the reference to get the gallery socialGrainGallery { title description } } } ``` ### Reverse Joins (Backlinks) When other records reference this record via `at-uri` fields, you get **plural** fields that find all records pointing here. **Lexicon Schema (social.grain.favorite):** ```json { "lexicon": 1, "id": "social.grain.favorite", "defs": { "main": { "type": "record", "key": "tid", "record": { "type": "object", "required": ["subject", "createdAt"], "properties": { "subject": { "type": "string", "format": "at-uri" }, "createdAt": { "type": "string", "format": "datetime" } } } } } } ``` **Generated GraphQL Types:** ```graphql type SocialGrainFavorite { uri: String! subject: String! # at-uri pointing to gallery createdAt: String! # Forward join (follows the subject field) socialGrainGallery: SocialGrainGallery } type SocialGrainGallery { uri: String! title: String # Auto-generated reverse joins (plural) # These find all records whose at-uri fields point here socialGrainFavorites: [SocialGrainFavorite!]! socialGrainComments: [SocialGrainComment!]! socialGrainGalleryItems: [SocialGrainGalleryItem!]! } ``` **Example Query:** ```graphql query { socialGrainGalleries(where: { actorHandle: { eq: "chadtmiller.com" } }) { edges { node { uri title # Get all favorites for this gallery socialGrainFavorites { uri createdAt actorHandle } # Get all comments for this gallery socialGrainComments { uri text actorHandle } } } } } ``` ### Count Fields For efficient counting without loading all data, use `*Count` fields: ```graphql query { socialGrainGalleries { edges { node { uri title # Efficient count queries (no data loading) socialGrainFavoritesCount socialGrainCommentsCount socialGrainPhotosCount } } } } ``` ### Combining Counts and Data Best practice: Get counts separately from limited data: ```graphql query { socialGrainGalleries { edges { node { uri title # Total count socialGrainFavoritesCount socialGrainCommentsCount # Show preview (first 3) socialGrainFavorites(limit: 3) { uri actorHandle } socialGrainComments(limit: 3) { uri text } } } } } ``` ## DataLoader & Performance The GraphQL API uses DataLoader for efficient batching: ### CollectionDidLoader - Batches queries by `(slice_uri, collection, did)` - Used for forward joins where the DID is known - Eliminates N+1 queries when following references ### CollectionUriLoader - Batches queries by `(slice_uri, collection, parent_uri, reference_field)` - Used for reverse joins based on at-uri fields - Efficiently loads all records that reference a parent URI - Supports multiple at-uri fields (tries each until match found) Example: Loading 100 galleries with favorites - **Without DataLoader**: 1 + 100 queries (N+1 problem) - **With DataLoader**: 1 + 1 query (batched) ## Complex Queries ### Nested Relationships ```graphql query { socialGrainGalleries { edges { node { title socialGrainGalleryItems { position socialGrainPhoto { uri alt photo { url(preset: "feed_fullsize") } socialGrainPhotoExifs { fNumber iSO make model } } } } } } } ``` ### Full Example ```graphql query MyGrainGalleries { socialGrainGalleries( where: { actorHandle: { eq: "chadtmiller.com" } } sortBy: [{ field: createdAt, direction: desc }] ) { edges { node { uri title description createdAt # Counts socialGrainFavoritesCount socialGrainCommentsCount # Preview data socialGrainFavorites(limit: 5) { uri createdAt actorHandle } socialGrainComments(limit: 3) { uri text createdAt actorHandle } # Gallery items with nested photos socialGrainGalleryItems { position socialGrainPhoto { uri alt photo { url(preset: "avatar") } aspectRatio createdAt socialGrainPhotoExifs { fNumber iSO make model } } } } } pageInfo { hasNextPage endCursor } } } ``` ## Mutations ### Upload Blob Upload a blob (image, video, or other file) to your AT Protocol repository. The blob will be stored in your PDS and can be referenced in records. ```graphql mutation UploadBlob($data: String!, $mimeType: String!) { uploadBlob(data: $data, mimeType: $mimeType) { blob { ref mimeType size } } } ``` **Parameters:** - `data` (String, required): Base64-encoded file data - `mimeType` (String, required): MIME type of the file (e.g., "image/jpeg", "image/png", "video/mp4") **Returns:** - `blob`: A Blob object containing: - `ref` (String): The CID (content identifier) reference for the blob - `mimeType` (String): The MIME type of the uploaded blob - `size` (Int): The size of the blob in bytes - `url` (String): CDN URL for the blob (supports presets) **Example with Variables:** ```json { "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "mimeType": "image/png" } ``` **Usage in Records:** After uploading a blob, use the returned blob object in your record mutations. You can provide the blob as a complete object with `ref` as a String: ```graphql mutation UpdateProfile($avatar: JSON) { updateAppBskyActorProfile( rkey: "self" input: { displayName: "My Name" avatar: $avatar # Blob object with ref as String (CID) } ) { uri displayName avatar { ref # Returns as String (CID) mimeType size url(preset: "avatar") } } } ``` **Example blob object for mutations:** ```json { "ref": "bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua", "mimeType": "image/jpeg", "size": 245678 } ``` **Note:** The GraphQL API automatically handles the conversion between the GraphQL format (where `ref` is a String containing the CID) and the AT Protocol format (where `ref` is an object `{$link: "cid"}`). You always work with `ref` as a simple String in GraphQL queries and mutations. ### Create Records Create new records in your AT Protocol repository. Each collection has a typed `create{Collection}` mutation with a corresponding `{Collection}Input` type. ```graphql mutation CreateFollow { createAppBskyGraphFollow( input: { subject: "did:plc:z72i7hdynmk6r22z27h6tvur" createdAt: "2025-01-15T12:00:00Z" } ) { uri cid subject createdAt } } ``` **Parameters:** - `input` (required): Typed input object with the fields defined in your lexicon - `rkey` (optional): Record key for the new record. If not provided, a TID (timestamp identifier) is automatically generated **Returns:** The complete created record with all fields, including generated fields like `uri`, `cid`, `did`, and `indexedAt`. **Example with custom rkey:** ```graphql mutation CreateFollowWithRkey { createAppBskyGraphFollow( input: { subject: "did:plc:z72i7hdynmk6r22z27h6tvur" createdAt: "2025-01-15T12:00:00Z" } rkey: "my-custom-key" ) { uri subject createdAt } } ``` ### Update Records Update existing records by their rkey. Each collection has an `update{Collection}` mutation. **Important:** Updates replace the entire record. You must provide all required fields, not just the fields you want to change. ```graphql mutation UpdateProfile { updateAppBskyActorProfile( rkey: "self" input: { displayName: "New Display Name" description: "Updated bio" avatar: $avatarBlob banner: $bannerBlob } ) { uri displayName description avatar { url(preset: "avatar") } } } ``` **Parameters:** - `rkey` (String, required): The record key of the record to update - `input` (required): Complete record data (all required fields must be provided) **Returns:** The complete updated record with all fields. **Notes:** - Updates perform a full record replacement via AT Protocol's `putRecord` - All required fields from your lexicon must be included in `input` - To partially update, first fetch the existing record, merge your changes, then update with complete data - The rkey is the last segment of the record's URI (e.g., `at://did:plc:abc/app.bsky.actor.profile/self` → rkey is `self`) ### Delete Records Delete records by their rkey. Each collection has a `delete{Collection}` mutation. ```graphql mutation DeleteFollow { deleteAppBskyGraphFollow(rkey: "3kjvbfz5nw42a") { uri subject } } ``` **Parameters:** - `rkey` (String, required): The record key of the record to delete **Returns:** The deleted record with its data (before deletion). **Example - Delete a profile:** ```graphql mutation DeleteProfile { deleteAppBskyActorProfile(rkey: "self") { uri displayName } } ``` ### Naming Convention All mutations follow a consistent naming pattern based on the lexicon collection name: | Collection | Create | Update | Delete | |------------|--------|--------|--------| | `app.bsky.actor.profile` | `createAppBskyActorProfile` | `updateAppBskyActorProfile` | `deleteAppBskyActorProfile` | | `app.bsky.graph.follow` | `createAppBskyGraphFollow` | `updateAppBskyGraphFollow` | `deleteAppBskyGraphFollow` | | `social.grain.gallery` | `createSocialGrainGallery` | `updateSocialGrainGallery` | `deleteSocialGrainGallery` | | `social.grain.photo` | `createSocialGrainPhoto` | `updateSocialGrainPhoto` | `deleteSocialGrainPhoto` | The pattern is: `{action}{PascalCaseCollection}` where dots in the collection name are removed and each segment is capitalized. ## Subscriptions Real-time updates for record changes. Each collection has three subscription fields: ### Created Records Subscribe to newly created records: ```graphql subscription { socialGrainGalleryCreated { uri title description createdAt } } ``` ### Updated Records Subscribe to record updates: ```graphql subscription { socialGrainGalleryUpdated { uri title description updatedAt } } ``` ### Deleted Records Subscribe to record deletions (returns just the URI): ```graphql subscription { socialGrainGalleryDeleted } ``` ## Limits & Performance - **Depth Limit**: 50 (supports introspection with circular relationships) - **Complexity Limit**: 5000 (prevents expensive queries) - **Default Limit**: 50 records per query - **DataLoader**: Automatic batching eliminates N+1 queries ## Best Practices 1. **Use count fields** when you only need totals 2. **Limit nested data** with `limit` parameter 3. **Request only needed fields** (no over-fetching) 4. **Use cursors** for pagination, not offset 5. **Batch related queries** with DataLoader (automatic) 6. **Combine counts + limited data** for previews ## Error Handling GraphQL errors include: - `"Query is nested too deep"` - Exceeds depth limit (50) - `"Query is too complex"` - Exceeds complexity limit (5000) - `"Schema error"` - Invalid slice or missing lexicons