···1920# HTTP client and server
21reqwest = { version = "0.12", features = ["json", "stream"] }
22-axum = { version = "0.7", features = ["ws", "macros"] }
23-axum-extra = { version = "0.9", features = ["form"] }
24tower = "0.5"
25tower-http = { version = "0.6", features = ["cors", "trace"] }
26···6566# Redis for caching
67redis = { version = "0.32", features = ["tokio-comp", "connection-manager"] }
00000
···1920# HTTP client and server
21reqwest = { version = "0.12", features = ["json", "stream"] }
22+axum = { version = "0.8", features = ["ws", "macros"] }
23+axum-extra = { version = "0.10", features = ["form"] }
24tower = "0.5"
25tower-http = { version = "0.6", features = ["cors", "trace"] }
26···6566# Redis for caching
67redis = { version = "0.32", features = ["tokio-comp", "connection-manager"] }
68+69+# GraphQL server
70+async-graphql = { version = "7.0", features = ["dynamic-schema", "dataloader"] }
71+async-graphql-axum = "7.0"
72+lazy_static = "1.5"
+35
api/src/database/actors.rs
···251 .await?;
252 Ok(result.rows_affected())
253 }
00000000000000000000000000000000000254}
255256/// Builds WHERE conditions specifically for actor queries.
···251 .await?;
252 Ok(result.rows_affected())
253 }
254+255+ /// Resolves actor handles to DIDs for a specific slice.
256+ ///
257+ /// # Arguments
258+ /// * `handles` - List of handles to resolve
259+ /// * `slice_uri` - AT-URI of the slice
260+ ///
261+ /// # Returns
262+ /// Vec of DIDs corresponding to the handles
263+ pub async fn resolve_handles_to_dids(
264+ &self,
265+ handles: &[String],
266+ slice_uri: &str,
267+ ) -> Result<Vec<String>, DatabaseError> {
268+ if handles.is_empty() {
269+ return Ok(Vec::new());
270+ }
271+272+ let placeholders: Vec<String> = (1..=handles.len())
273+ .map(|i| format!("${}", i))
274+ .collect();
275+ let query_sql = format!(
276+ "SELECT DISTINCT did FROM actor WHERE handle = ANY(ARRAY[{}]) AND slice_uri = ${}",
277+ placeholders.join(", "),
278+ handles.len() + 1
279+ );
280+281+ let mut query = sqlx::query_scalar::<_, String>(&query_sql);
282+ for handle in handles {
283+ query = query.bind(handle);
284+ }
285+ query = query.bind(slice_uri);
286+287+ Ok(query.fetch_all(&self.pool).await?)
288+ }
289}
290291/// Builds WHERE conditions specifically for actor queries.
+27
api/src/graphql/dataloaders.rs
···000000000000000000000000000
···1+//! DataLoader utilities for extracting references from records
2+3+use serde_json::Value;
4+5+/// Extract URI from a strongRef value
6+/// strongRef format: { "$type": "com.atproto.repo.strongRef", "uri": "at://...", "cid": "..." }
7+pub fn extract_uri_from_strong_ref(value: &Value) -> Option<String> {
8+ if let Some(obj) = value.as_object() {
9+ // Check if this is a strongRef
10+ if let Some(type_val) = obj.get("$type") {
11+ if type_val.as_str() == Some("com.atproto.repo.strongRef") {
12+ return obj.get("uri").and_then(|u| u.as_str()).map(|s| s.to_string());
13+ }
14+ }
15+16+ // Also support direct uri field (some lexicons might use this)
17+ if let Some(uri) = obj.get("uri") {
18+ if let Some(uri_str) = uri.as_str() {
19+ if uri_str.starts_with("at://") {
20+ return Some(uri_str.to_string());
21+ }
22+ }
23+ }
24+ }
25+26+ None
27+}
···1+//! GraphQL endpoint implementation for Slices
2+//!
3+//! This module provides a GraphQL interface to query slice records with support
4+//! for joining linked records through AT Protocol strongRef references.
5+6+mod schema_builder;
7+mod dataloaders;
8+mod types;
9+pub mod handler;
10+11+pub use schema_builder::build_graphql_schema;
12+pub use handler::{graphql_handler, graphql_playground};