Auto-indexing service and GraphQL API for AT Protocol Records
at main 255 lines 5.7 kB view raw view rendered
1# Joins 2 3AT Protocol data lives in collections. A user's status records (`xyz.statusphere.status`) occupy one collection, their profile (`app.bsky.actor.profile`) another. Quickslice generates joins that query across collections—fetch a status and its author's profile in one request. 4 5## Join Types 6 7Quickslice generates three join types automatically: 8 9| Type | What it does | Field naming | 10|------|--------------|--------------| 11| **Forward** | Follows a URI or strong ref to another record | `{fieldName}Resolved` | 12| **Reverse** | Finds all records that reference a given record | `{SourceType}Via{FieldName}` | 13| **DID** | Finds records by the same author | `{CollectionName}ByDid` | 14 15## Forward Joins 16 17Forward joins follow references from one record to another. When a record has a field containing an AT-URI or strong ref, Quickslice generates a `{fieldName}Resolved` field that fetches the referenced record. 18 19### Example: Resolving a Favorite's Subject 20 21A favorite record has a `subject` field containing an AT-URI. The `subjectResolved` field fetches the actual record: 22 23```graphql 24query { 25 socialGrainFavorite(first: 5) { 26 edges { 27 node { 28 subject 29 createdAt 30 subjectResolved { 31 ... on SocialGrainGallery { 32 uri 33 title 34 } 35 } 36 } 37 } 38 } 39} 40``` 41 42Forward joins return a `Record` union type because the referenced record could be any type. Use inline fragments (`... on TypeName`) for type-specific fields. 43 44## Reverse Joins 45 46Reverse joins work oppositely: given a record, find all records that reference it. Quickslice analyzes your Lexicons and generates reverse join fields automatically. 47 48Reverse joins return paginated connections supporting filtering, sorting, and cursors. 49 50### Example: Comments on a Photo 51 52Find all comments that reference a specific photo: 53 54```graphql 55query { 56 socialGrainPhoto(first: 5) { 57 edges { 58 node { 59 uri 60 alt 61 socialGrainCommentViaSubject(first: 10) { 62 totalCount 63 edges { 64 node { 65 text 66 createdAt 67 } 68 } 69 pageInfo { 70 hasNextPage 71 endCursor 72 } 73 } 74 } 75 } 76 } 77} 78``` 79 80### Sorting and Filtering Reverse Joins 81 82Reverse joins support the same sorting and filtering as top-level queries: 83 84```graphql 85query { 86 socialGrainGallery(first: 3) { 87 edges { 88 node { 89 title 90 socialGrainGalleryItemViaGallery( 91 first: 10 92 sortBy: [{ field: position, direction: ASC }] 93 where: { createdAt: { gt: "2025-01-01T00:00:00Z" } } 94 ) { 95 edges { 96 node { 97 position 98 } 99 } 100 } 101 } 102 } 103 } 104} 105``` 106 107## DID Joins 108 109DID joins connect records by author identity. Every record has a `did` field identifying its creator. Quickslice generates `{CollectionName}ByDid` fields to find related records by the same author. 110 111### Example: Author Profile from a Status 112 113Get the author's profile alongside their status: 114 115```graphql 116query { 117 xyzStatusphereStatus(first: 10) { 118 edges { 119 node { 120 status 121 createdAt 122 appBskyActorProfileByDid { 123 displayName 124 avatar { url } 125 } 126 } 127 } 128 } 129} 130``` 131 132### Unique vs Non-Unique DID Joins 133 134Some collections have one record per DID (like profiles with a `literal:self` key). These return a single object: 135 136```graphql 137appBskyActorProfileByDid { 138 displayName 139} 140``` 141 142Other collections can have multiple records per DID. These return paginated connections: 143 144```graphql 145socialGrainPhotoByDid(first: 10, sortBy: [{ field: createdAt, direction: DESC }]) { 146 totalCount 147 edges { 148 node { 149 alt 150 } 151 } 152} 153``` 154 155### Cross-Lexicon DID Joins 156 157DID joins work across different Lexicon families. Get a user's Bluesky profile alongside their app-specific data: 158 159```graphql 160query { 161 socialGrainPhoto(first: 5) { 162 edges { 163 node { 164 alt 165 appBskyActorProfileByDid { 166 displayName 167 avatar { url } 168 } 169 socialGrainActorProfileByDid { 170 description 171 } 172 } 173 } 174 } 175} 176``` 177 178## Common Patterns 179 180### Profile Lookups 181 182The most common pattern: joining author profiles to any record type. 183 184```graphql 185query { 186 myAppPost(first: 20) { 187 edges { 188 node { 189 content 190 appBskyActorProfileByDid { 191 displayName 192 avatar { url } 193 } 194 } 195 } 196 } 197} 198``` 199 200### Engagement Counts 201 202Use reverse joins to count likes, comments, or other engagement: 203 204```graphql 205query { 206 socialGrainPhoto(first: 10) { 207 edges { 208 node { 209 uri 210 socialGrainFavoriteViaSubject { 211 totalCount 212 } 213 socialGrainCommentViaSubject { 214 totalCount 215 } 216 } 217 } 218 } 219} 220``` 221 222### User Activity 223 224Get all records by a user across multiple collections: 225 226```graphql 227query { 228 socialGrainActorProfile(first: 1, where: { actorHandle: { eq: "alice.bsky.social" } }) { 229 edges { 230 node { 231 displayName 232 socialGrainPhotoByDid(first: 5) { 233 totalCount 234 edges { node { alt } } 235 } 236 socialGrainGalleryByDid(first: 5) { 237 totalCount 238 edges { node { title } } 239 } 240 } 241 } 242 } 243} 244``` 245 246## How Batching Works 247 248Quickslice batches join resolution to avoid the N+1 query problem. When querying 100 photos with author profiles: 249 2501. Fetches 100 photos in one query 2512. Collects all unique DIDs from those photos 2523. Fetches all profiles in a single query: `WHERE did IN (...)` 2534. Maps profiles back to their photos 254 255All join types batch automatically.