Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GraphQL Integration

GraphQL provides a powerful alternative to REST APIs, allowing clients to request exactly the data they need. This chapter covers how to integrate GraphQL into your Oxidite applications.

Overview

Oxidite’s GraphQL integration includes:

  • Schema definition with Rust types
  • Query and mutation resolvers
  • Subscription support
  • Integration with Oxidite’s routing system
  • Type safety with Juniper integration
  • Real-time subscriptions

Basic GraphQL Setup

Set up a basic GraphQL endpoint:

use oxidite::prelude::*;
use juniper::{EmptyMutation, EmptySubscription, RootNode};

// Define a simple user object
#[derive(juniper::GraphQLObject)]
#[graphql(description = "A user in the system")]
struct User {
    id: juniper::ID,
    name: String,
    email: String,
    created_at: String,
}

// Define the query root
struct QueryRoot;

#[juniper::graphql_object]
impl QueryRoot {
    /// Get a user by ID
    async fn user(id: juniper::ID) -> Option<User> {
        // In a real app, fetch from database
        if id == juniper::ID::from("1") {
            Some(User {
                id: id.clone(),
                name: "John Doe".to_string(),
                email: "john@example.com".to_string(),
                created_at: chrono::Utc::now().to_rfc3339(),
            })
        } else {
            None
        }
    }
    
    /// Get all users
    async fn users() -> Vec<User> {
        vec![
            User {
                id: juniper::ID::from("1"),
                name: "John Doe".to_string(),
                email: "john@example.com".to_string(),
                created_at: chrono::Utc::now().to_rfc3339(),
            },
            User {
                id: juniper::ID::from("2"),
                name: "Jane Smith".to_string(),
                email: "jane@example.com".to_string(),
                created_at: chrono::Utc::now().to_rfc3339(),
            },
        ]
    }
}

// Create the schema
type Schema = juniper::RootNode<'static, QueryRoot, EmptyMutation, EmptySubscription>;

fn create_schema() -> Schema {
    Schema::new(QueryRoot, EmptyMutation::new(), EmptySubscription::new())
}

// GraphQL endpoint handler
async fn graphql_handler(
    mut req: Request,
    State(schema): State<Schema>
) -> Result<Response> {
    // Collect the request body
    use http_body_util::BodyExt;
    let body_bytes = req
        .body_mut()
        .collect()
        .await
        .map_err(|e| Error::InternalServerError(e.to_string()))?
        .to_bytes();
    
    let body_str = String::from_utf8_lossy(&body_bytes);
    
    // Parse GraphQL request
    let gql_request: juniper::http::GraphQLRequest = 
        serde_json::from_str(&body_str)
            .map_err(|e| Error::BadRequest(format!("Invalid GraphQL request: {}", e)))?;
    
    // Execute the request
    let context = DatabaseContext {}; // Context for resolvers
    let response = gql_request.execute(&schema, &context).await;
    
    // Return response
    let json_response = serde_json::to_string(&response)
        .map_err(|e| Error::InternalServerError(format!("Serialization error: {}", e)))?;
    
    Ok(Response::json(serde_json::Value::from(response)))
}

// Context for GraphQL resolvers
struct DatabaseContext;

// In a real app, implement your database access here

Advanced Schema Definition

Define more complex schemas with mutations and relationships:

use oxidite::prelude::*;
use juniper::{FieldResult, GraphQLInputObject};

// Enhanced user with more fields
#[derive(juniper::GraphQLObject, Clone)]
#[graphql(description = "A user in the system")]
struct User {
    id: juniper::ID,
    name: String,
    email: String,
    age: i32,
    posts: Vec<Post>,
    created_at: String,
}

// Post object
#[derive(juniper::GraphQLObject, Clone)]
#[graphql(description = "A blog post")]
struct Post {
    id: juniper::ID,
    title: String,
    content: String,
    author: User,
    published: bool,
    created_at: String,
}

// Input object for mutations
#[derive(GraphQLInputObject)]
#[graphql(description = "Properties for creating a new user")]
struct NewUser {
    name: String,
    email: String,
    age: i32,
}

// Input object for creating a post
#[derive(GraphQLInputObject)]
#[graphql(description = "Properties for creating a new post")]
struct NewPost {
    title: String,
    content: String,
    author_id: juniper::ID,
}

// Enhanced query root
struct QueryRoot;

#[juniper::graphql_object]
impl QueryRoot {
    /// Get a user by ID
    async fn user(id: juniper::ID, context: &DatabaseContext) -> FieldResult<Option<User>> {
        // In a real app, fetch from database
        Ok(Some(User {
            id: id.clone(),
            name: "John Doe".to_string(),
            email: "john@example.com".to_string(),
            age: 30,
            posts: vec![],
            created_at: chrono::Utc::now().to_rfc3339(),
        }))
    }
    
    /// Get all users
    async fn users(context: &DatabaseContext) -> FieldResult<Vec<User>> {
        Ok(vec![
            User {
                id: juniper::ID::from("1"),
                name: "John Doe".to_string(),
                email: "john@example.com".to_string(),
                age: 30,
                posts: vec![],
                created_at: chrono::Utc::now().to_rfc3339(),
            },
            User {
                id: juniper::ID::from("2"),
                name: "Jane Smith".to_string(),
                email: "jane@example.com".to_string(),
                age: 25,
                posts: vec![],
                created_at: chrono::Utc::now().to_rfc3339(),
            },
        ])
    }
    
    /// Get a post by ID
    async fn post(id: juniper::ID, context: &DatabaseContext) -> FieldResult<Option<Post>> {
        Ok(Some(Post {
            id: id.clone(),
            title: "Sample Post".to_string(),
            content: "This is a sample post content.".to_string(),
            author: User {
                id: juniper::ID::from("1"),
                name: "John Doe".to_string(),
                email: "john@example.com".to_string(),
                age: 30,
                posts: vec![],
                created_at: chrono::Utc::now().to_rfc3339(),
            },
            published: true,
            created_at: chrono::Utc::now().to_rfc3339(),
        }))
    }
    
    /// Get all posts
    async fn posts(context: &DatabaseContext) -> FieldResult<Vec<Post>> {
        Ok(vec![
            Post {
                id: juniper::ID::from("1"),
                title: "First Post".to_string(),
                content: "Content of the first post.".to_string(),
                author: User {
                    id: juniper::ID::from("1"),
                    name: "John Doe".to_string(),
                    email: "john@example.com".to_string(),
                    age: 30,
                    posts: vec![],
                    created_at: chrono::Utc::now().to_rfc3339(),
                },
                published: true,
                created_at: chrono::Utc::now().to_rfc3339(),
            },
        ])
    }
}

// Mutation root
struct MutationRoot;

#[juniper::graphql_object]
impl MutationRoot {
    /// Create a new user
    async fn create_user(
        new_user: NewUser,
        context: &DatabaseContext,
    ) -> FieldResult<User> {
        // In a real app, save to database
        Ok(User {
            id: juniper::ID::from(uuid::Uuid::new_v4().to_string()),
            name: new_user.name,
            email: new_user.email,
            age: new_user.age,
            posts: vec![],
            created_at: chrono::Utc::now().to_rfc3339(),
        })
    }
    
    /// Create a new post
    async fn create_post(
        new_post: NewPost,
        context: &DatabaseContext,
    ) -> FieldResult<Post> {
        // In a real app, save to database
        Ok(Post {
            id: juniper::ID::from(uuid::Uuid::new_v4().to_string()),
            title: new_post.title,
            content: new_post.content,
            author: User {
                id: new_post.author_id,
                name: "Author Name".to_string(),
                email: "author@example.com".to_string(),
                age: 30,
                posts: vec![],
                created_at: chrono::Utc::now().to_rfc3339(),
            },
            published: false,
            created_at: chrono::Utc::now().to_rfc3339(),
        })
    }
    
    /// Update a user
    async fn update_user(
        id: juniper::ID,
        name: Option<String>,
        email: Option<String>,
        age: Option<i32>,
        context: &DatabaseContext,
    ) -> FieldResult<Option<User>> {
        // In a real app, update in database
        Ok(Some(User {
            id,
            name: name.unwrap_or_else(|| "John Doe".to_string()),
            email: email.unwrap_or_else(|| "john@example.com".to_string()),
            age: age.unwrap_or(30),
            posts: vec![],
            created_at: chrono::Utc::now().to_rfc3339(),
        }))
    }
    
    /// Delete a user
    async fn delete_user(
        id: juniper::ID,
        context: &DatabaseContext,
    ) -> FieldResult<bool> {
        // In a real app, delete from database
        Ok(true) // Simulate successful deletion
    }
}

type Schema = juniper::RootNode<'static, QueryRoot, MutationRoot, EmptySubscription>;

fn create_advanced_schema() -> Schema {
    Schema::new(QueryRoot, MutationRoot, EmptySubscription::new())
}

Integration with Oxidite Routing

Integrate GraphQL with Oxidite’s routing system:

use oxidite::prelude::*;
use std::sync::Arc;

// Enhanced GraphQL handler with proper request/response handling
async fn graphql_endpoint(
    mut req: Request,
    State(schema): State<Arc<Schema>>
) -> Result<Response> {
    match req.method().as_str() {
        "GET" => {
            // Serve GraphQL Playground/GraphiQL in development
            serve_graphql_playground()
        }
        "POST" => {
            // Handle GraphQL query
            handle_graphql_request(req, schema.as_ref()).await
        }
        _ => Err(Error::MethodNotAllowed),
    }
}

async fn handle_graphql_request(req: Request, schema: &Schema) -> Result<Response> {
    use http_body_util::BodyExt;
    
    // Collect the request body
    let body_bytes = req
        .into_body()
        .collect()
        .await
        .map_err(|e| Error::InternalServerError(e.to_string()))?
        .to_bytes();
    
    let body_str = String::from_utf8_lossy(&body_bytes);
    
    // Parse GraphQL request
    let gql_request: juniper::http::GraphQLRequest = 
        serde_json::from_str(&body_str)
            .map_err(|e| Error::BadRequest(format!("Invalid GraphQL request: {}", e)))?;
    
    // Execute the request
    let context = DatabaseContext {};
    let response = gql_request.execute(schema, &context).await;
    
    // Create response
    let json_response = serde_json::Value::from(response);
    
    if json_response.get("errors").is_some() {
        Ok(Response::json(json_response))
    } else {
        Ok(Response::json(json_response))
    }
}

fn serve_graphql_playground() -> Result<Response> {
    let html = r#"
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset=utf-8/>
        <title>GraphQL Playground</title>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
        <link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png" />
        <script src="https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
    </head>
    <body>
        <div id="root">
            <style>
                body {
                    background-color: rgb(23, 42, 58);
                    font-family: 'Open Sans', sans-serif;
                    height: 90vh;
                    margin: 0;
                    overflow: hidden;
                    width: 100vw;
                }
                #root {
                    height: 100%;
                    width: 100%;
                }
                .loading {
                    align-items: center;
                    display: flex;
                    justify-content: center;
                    height: 100%;
                    width: 100%;
                }
                .loading img {
                    animation: loadingAnimation 1s infinite alternate;
                }
                @keyframes loadingAnimation {
                    0% { opacity: 0.3; }
                    100% { opacity: 1; }
                }
            </style>
            <div class="loading">
                <img src='https://cdn.jsdelivr.net/npm/graphql-playground-react/build/logo.png' alt=''>
            </div>
        </div>
        <script>
            window.addEventListener('load', function (event) {
                const root = document.getElementById('root');
                const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
                GraphQLPlayground.init(root, {
                    endpoint: location.href,
                    subscriptionsEndpoint: `${wsProto}//${location.host}${location.pathname}`
                });
            });
        </script>
    </body>
    </html>
    "#;
    
    Ok(Response::html(html.to_string()))
}

// Initialize the application with GraphQL
#[tokio::main]
async fn main() -> Result<()> {
    let schema = Arc::new(create_advanced_schema());
    
    let mut router = Router::new();
    
    // Add GraphQL endpoint
    router.post("/graphql")
        .with_state(schema.clone())
        .handler(graphql_endpoint);
    
    router.get("/graphql")
        .with_state(schema)
        .handler(graphql_endpoint);
    
    Server::new(router)
        .listen("127.0.0.1:3000".parse()?)
        .await
}

Database Integration

Connect GraphQL resolvers to your database:

use oxidite::prelude::*;
use oxidite_db::Model;
use serde::{Deserialize, Serialize};

// Define models that match your GraphQL types
#[derive(Model, Serialize, Deserialize, juniper::GraphQLObject)]
#[model(table = "graphql_users")]
#[graphql(description = "A user in the system")]
pub struct GraphqlUser {
    #[model(primary_key)]
    pub id: i32,
    #[model(not_null)]
    pub name: String,
    #[model(unique, not_null)]
    pub email: String,
    pub age: i32,
    #[model(created_at)]
    pub created_at: String,
}

#[derive(Model, Serialize, Deserialize, juniper::GraphQLObject)]
#[model(table = "graphql_posts")]
#[graphql(description = "A blog post")]
pub struct GraphqlPost {
    #[model(primary_key)]
    pub id: i32,
    #[model(not_null)]
    pub title: String,
    #[model(not_null)]
    pub content: String,
    pub author_id: i32,
    pub published: bool,
    #[model(created_at)]
    pub created_at: String,
}

// Enhanced context with database access
struct DatabaseContext {
    // In a real app, this would contain database connection
}

// Query resolvers that use the database
struct DbQueryRoot;

#[juniper::graphql_object(Context = DatabaseContext)]
impl DbQueryRoot {
    /// Get a user by ID
    async fn user(id: i32, context: &DatabaseContext) -> FieldResult<Option<GraphqlUser>> {
        // In a real app, fetch from database
        let user = GraphqlUser::find_by_id(id).await
            .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))?;
        
        Ok(user)
    }
    
    /// Get all users
    async fn users(context: &DatabaseContext) -> FieldResult<Vec<GraphqlUser>> {
        let users = GraphqlUser::find_all().await
            .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))?;
        
        Ok(users)
    }
    
    /// Get a post by ID
    async fn post(id: i32, context: &DatabaseContext) -> FieldResult<Option<GraphqlPost>> {
        let post = GraphqlPost::find_by_id(id).await
            .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))?;
        
        Ok(post)
    }
    
    /// Get posts by author
    async fn posts_by_author(
        author_id: i32,
        context: &DatabaseContext
    ) -> FieldResult<Vec<GraphqlPost>> {
        let posts = GraphqlPost::find_where(&format!("author_id = {}", author_id)).await
            .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))?;
        
        Ok(posts)
    }
}

// Mutation resolvers that modify the database
struct DbMutationRoot;

#[juniper::graphql_object(Context = DatabaseContext)]
impl DbMutationRoot {
    /// Create a new user
    async fn create_user(
        name: String,
        email: String,
        age: i32,
        context: &DatabaseContext,
    ) -> FieldResult<GraphqlUser> {
        let user = GraphqlUser {
            id: 0, // Will be auto-generated
            name,
            email,
            age,
            created_at: chrono::Utc::now().to_rfc3339(),
        };
        
        let saved_user = user.save().await
            .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))?;
        
        Ok(saved_user)
    }
    
    /// Update a user
    async fn update_user(
        id: i32,
        name: Option<String>,
        email: Option<String>,
        age: Option<i32>,
        context: &DatabaseContext,
    ) -> FieldResult<Option<GraphqlUser>> {
        if let Some(mut user) = GraphqlUser::find_by_id(id).await.map_err(|e| {
            juniper::FieldError::new(e.to_string(), juniper::Value::null())
        })? {
            if let Some(new_name) = name {
                user.name = new_name;
            }
            if let Some(new_email) = email {
                user.email = new_email;
            }
            if let Some(new_age) = age {
                user.age = new_age;
            }
            
            let updated_user = user.save().await
                .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))?;
            
            Ok(Some(updated_user))
        } else {
            Ok(None)
        }
    }
    
    /// Delete a user
    async fn delete_user(
        id: i32,
        context: &DatabaseContext,
    ) -> FieldResult<bool> {
        if let Some(user) = GraphqlUser::find_by_id(id).await.map_err(|e| {
            juniper::FieldError::new(e.to_string(), juniper::Value::null())
        })? {
            user.delete().await
                .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))?;
            Ok(true)
        } else {
            Ok(false)
        }
    }
}

type DbSchema = juniper::RootNode<'static, DbQueryRoot, DbMutationRoot, EmptySubscription>;

fn create_db_schema() -> DbSchema {
    DbSchema::new(DbQueryRoot, DbMutationRoot, EmptySubscription::new())
}

Authentication and Authorization

Secure your GraphQL endpoints:

use oxidite::prelude::*;

// Context with authentication info
struct AuthenticatedContext {
    user: Option<GraphqlUser>,
}

// Secured query root
struct SecuredQueryRoot;

#[juniper::graphql_object(Context = AuthenticatedContext)]
impl SecuredQueryRoot {
    /// Get current user (requires authentication)
    async fn me(context: &AuthenticatedContext) -> FieldResult<Option<GraphqlUser>> {
        match &context.user {
            Some(user) => Ok(Some(user.clone())),
            None => Err(juniper::FieldError::new(
                "Authentication required",
                juniper::Value::null()
            )),
        }
    }
    
    /// Get users (requires admin role)
    async fn users(context: &AuthenticatedContext) -> FieldResult<Vec<GraphqlUser>> {
        match &context.user {
            Some(user) if is_admin_user(user) => {
                GraphqlUser::find_all().await
                    .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))
            }
            Some(_) => Err(juniper::FieldError::new(
                "Admin role required",
                juniper::Value::null()
            )),
            None => Err(juniper::FieldError::new(
                "Authentication required",
                juniper::Value::null()
            )),
        }
    }
    
    /// Get user by ID (public endpoint)
    async fn user(id: i32, context: &AuthenticatedContext) -> FieldResult<Option<GraphqlUser>> {
        // Anyone can view user profiles
        GraphqlUser::find_by_id(id).await
            .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))
    }
}

// Secured mutation root
struct SecuredMutationRoot;

#[juniper::graphql_object(Context = AuthenticatedContext)]
impl SecuredMutationRoot {
    /// Create post (authenticated users only)
    async fn create_post(
        title: String,
        content: String,
        context: &AuthenticatedContext,
    ) -> FieldResult<GraphqlPost> {
        match &context.user {
            Some(user) => {
                let post = GraphqlPost {
                    id: 0,
                    title,
                    content,
                    author_id: user.id,
                    published: false,
                    created_at: chrono::Utc::now().to_rfc3339(),
                };
                
                post.save().await
                    .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))
            }
            None => Err(juniper::FieldError::new(
                "Authentication required to create posts",
                juniper::Value::null()
            )),
        }
    }
    
    /// Update own post (must be the author)
    async fn update_post(
        id: i32,
        title: Option<String>,
        content: Option<String>,
        published: Option<bool>,
        context: &AuthenticatedContext,
    ) -> FieldResult<Option<GraphqlPost>> {
        match &context.user {
            Some(current_user) => {
                if let Some(mut post) = GraphqlPost::find_by_id(id).await
                    .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))?
                {
                    // Check if current user is the author
                    if post.author_id != current_user.id {
                        return Err(juniper::FieldError::new(
                            "Only the author can update this post",
                            juniper::Value::null()
                        ));
                    }
                    
                    if let Some(new_title) = title {
                        post.title = new_title;
                    }
                    if let Some(new_content) = content {
                        post.content = new_content;
                    }
                    if let Some(new_published) = published {
                        post.published = new_published;
                    }
                    
                    let updated_post = post.save().await
                        .map_err(|e| juniper::FieldError::new(e.to_string(), juniper::Value::null()))?;
                    
                    Ok(Some(updated_post))
                } else {
                    Ok(None)
                }
            }
            None => Err(juniper::FieldError::new(
                "Authentication required to update posts",
                juniper::Value::null()
            )),
        }
    }
}

// Authentication middleware for GraphQL
async fn graphql_auth_middleware(
    mut req: Request,
    next: Next,
) -> Result<Response> {
    // Extract authentication token from headers
    let auth_header = req.headers()
        .get("authorization")
        .and_then(|hv| hv.to_str().ok());
    
    let mut context = AuthenticatedContext { user: None };
    
    if let Some(auth) = auth_header {
        if auth.starts_with("Bearer ") {
            let token = auth.trim_start_matches("Bearer ").trim();
            
            // Verify token and get user
            if let Ok(user_id) = verify_jwt_token(token).await {
                // Fetch user from database
                if let Ok(Some(user)) = GraphqlUser::find_by_id(user_id).await {
                    context.user = Some(user);
                }
            }
        }
    }
    
    // Add context to request extensions for GraphQL handler
    req.extensions_mut().insert(context);
    
    next.run(req).await
}

async fn verify_jwt_token(_token: &str) -> Result<i32, String> {
    // In a real app, verify the JWT token and return user ID
    // This is a placeholder implementation
    Ok(1)
}

fn is_admin_user(user: &GraphqlUser) -> bool {
    // In a real app, check user roles from database
    user.email == "admin@example.com"
}

type SecuredSchema = juniper::RootNode<'static, SecuredQueryRoot, SecuredMutationRoot, EmptySubscription>;

fn create_secured_schema() -> SecuredSchema {
    SecuredSchema::new(SecuredQueryRoot, SecuredMutationRoot, EmptySubscription::new())
}

Subscriptions

Implement real-time GraphQL subscriptions:

use oxidite::prelude::*;
use juniper::http::GraphQLRequest;
use futures::stream::Stream;
use tokio_stream::wrappers::UnboundedReceiverStream;
use serde::{Deserialize, Serialize};

// Define subscription types
#[derive(juniper::GraphQLObject)]
#[graphql(description = "A notification")]
struct Notification {
    id: juniper::ID,
    message: String,
    user_id: juniper::ID,
    created_at: String,
}

// Subscription root
struct SubscriptionRoot;

#[juniper::graphql_subscription]
impl SubscriptionRoot {
    /// Subscribe to notifications for a specific user
    async fn notifications(
        &self,
        user_id: juniper::ID,
    ) -> impl Stream<Item = Notification> {
        use tokio_stream::StreamExt;
        
        // Create a channel for sending notifications
        let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<Notification>();
        
        // Simulate sending notifications
        let user_id_clone = user_id.clone();
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
            
            for i in 1..=10 {
                interval.tick().await;
                
                let notification = Notification {
                    id: juniper::ID::from(format!("notif_{}", i)),
                    message: format!("Notification {} for user {}", i, user_id_clone),
                    user_id: user_id_clone.clone(),
                    created_at: chrono::Utc::now().to_rfc3339(),
                };
                
                if tx.send(notification).is_err() {
                    break; // Channel closed
                }
            }
        });
        
        UnboundedReceiverStream::new(rx)
    }
}

type SubscriptionSchema = juniper::RootNode<'static, SecuredQueryRoot, SecuredMutationRoot, SubscriptionRoot>;

fn create_subscription_schema() -> SubscriptionSchema {
    SubscriptionSchema::new(
        SecuredQueryRoot,
        SecuredMutationRoot,
        SubscriptionRoot,
    )
}

// WebSocket handler for subscriptions
async fn websocket_graphql_handler(
    ws: oxidite_realtime::websocket::WebSocket
) -> Result<()> {
    ws.on_message(|msg| async move {
        match msg {
            oxidite_realtime::websocket::Message::Text(text) => {
                // Parse GraphQL subscription message
                match serde_json::from_str::<SubscriptionMessage>(&text) {
                    Ok(sub_msg) => {
                        match sub_msg.r#type.as_str() {
                            "connection_init" => {
                                // Initialize connection
                                Ok(oxidite_realtime::websocket::Message::Text(
                                    r#"{"type":"connection_ack"}"#.to_string()
                                ))
                            }
                            "subscribe" => {
                                // Handle subscription request
                                // This would typically involve setting up a subscription
                                Ok(oxidite_realtime::websocket::Message::Text(
                                    r#"{"type":"next","id":"1","payload":{"data":{"hello":"world"}}}"#.to_string()
                                ))
                            }
                            "unsubscribe" => {
                                // Handle unsubscribe
                                Ok(oxidite_realtime::websocket::Message::Text(
                                    r#"{"type":"complete","id":"1"}"#.to_string()
                                ))
                            }
                            _ => Ok(oxidite_realtime::websocket::Message::Text(
                                r#"{"type":"error","payload":"Unknown message type"}"#.to_string()
                            ))
                        }
                    }
                    Err(_) => Ok(oxidite_realtime::websocket::Message::Text(
                        r#"{"type":"error","payload":"Invalid message format"}"#.to_string()
                    )),
                }
            }
            _ => Ok(msg), // Return other messages as-is
        }
    }).await?;
    
    Ok(())
}

#[derive(Deserialize, Serialize)]
struct SubscriptionMessage {
    r#type: String,
    id: Option<String>,
    payload: Option<serde_json::Value>,
}

Performance Optimization

Optimize GraphQL performance:

use oxidite::prelude::*;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;

// DataLoader pattern for efficient database access
struct DataLoader<T> {
    cache: Arc<RwLock<HashMap<i32, T>>>,
    batch_loader: Arc<dyn Fn(Vec<i32>) -> BoxFuture<Vec<T>> + Send + Sync>,
}

type BoxFuture<T> = std::pin::Pin<Box<dyn futures::Future<Output = T> + Send>>;

impl<T: Clone + Send + Sync + 'static> DataLoader<T> {
    fn new<F, Fut>(loader: F) -> Self
    where
        F: Fn(Vec<i32>) -> Fut + Send + Sync + 'static,
        Fut: futures::Future<Output = Vec<T>> + Send + 'static,
    {
        Self {
            cache: Arc::new(RwLock::new(HashMap::new())),
            batch_loader: Arc::new(move |keys| {
                let loader = loader.clone();
                Box::pin(loader(keys))
            }),
        }
    }
    
    async fn load(&self, key: i32) -> Option<T> {
        // Check cache first
        {
            let cache = self.cache.read().await;
            if let Some(item) = cache.get(&key) {
                return Some(item.clone());
            }
        }
        
        // Load in batch (simplified for example)
        let items = (self.batch_loader)(vec![key]).await;
        let item = items.into_iter().find(|_| true); // Simplified
        
        // Cache the result
        if let Some(ref item) = item {
            let mut cache = self.cache.write().await;
            cache.insert(key, item.clone());
        }
        
        item
    }
    
    async fn load_many(&self, keys: Vec<i32>) -> Vec<T> {
        let mut uncached_keys = Vec::new();
        let mut results = Vec::new();
        
        // Check cache for each key
        {
            let cache = self.cache.read().await;
            for key in &keys {
                if let Some(item) = cache.get(key) {
                    results.push(item.clone());
                } else {
                    uncached_keys.push(*key);
                }
            }
        }
        
        // Load uncached items in batch
        if !uncached_keys.is_empty() {
            let loaded_items = (self.batch_loader)(uncached_keys.clone()).await;
            
            // Add to cache and results
            let mut cache = self.cache.write().await;
            for (i, key) in uncached_keys.iter().enumerate() {
                if i < loaded_items.len() {
                    let item = loaded_items[i].clone();
                    cache.insert(*key, item.clone());
                    results.push(item);
                }
            }
        }
        
        results
    }
}

// Context with data loaders
struct OptimizedContext {
    user_loader: DataLoader<GraphqlUser>,
    post_loader: DataLoader<GraphqlPost>,
}

// Query root using data loaders
struct OptimizedQueryRoot;

#[juniper::graphql_object(Context = OptimizedContext)]
impl OptimizedQueryRoot {
    /// Get user with optimized loading
    async fn user(id: i32, context: &OptimizedContext) -> FieldResult<Option<GraphqlUser>> {
        let user = context.user_loader.load(id).await;
        Ok(user)
    }
    
    /// Get multiple users efficiently
    async fn users(ids: Vec<i32>, context: &OptimizedContext) -> FieldResult<Vec<GraphqlUser>> {
        let users = context.user_loader.load_many(ids).await;
        Ok(users)
    }
    
    /// Get posts by author with optimized loading
    async fn posts_by_author(
        author_id: i32,
        context: &OptimizedContext
    ) -> FieldResult<Vec<GraphqlPost>> {
        // In a real app, you'd have a specialized loader for this
        // For now, just return empty to satisfy the example
        Ok(vec![])
    }
}

// Schema with optimizations
type OptimizedSchema = juniper::RootNode<'static, OptimizedQueryRoot, SecuredMutationRoot, SubscriptionRoot>;

fn create_optimized_schema() -> OptimizedSchema {
    let user_loader = DataLoader::new(|ids| {
        Box::pin(async move {
            // In a real app, batch fetch users from database
            ids.into_iter()
                .map(|id| GraphqlUser {
                    id,
                    name: format!("User {}", id),
                    email: format!("user{}@example.com", id),
                    age: 25,
                    created_at: chrono::Utc::now().to_rfc3339(),
                })
                .collect()
        })
    });
    
    let post_loader = DataLoader::new(|ids| {
        Box::pin(async move {
            // In a real app, batch fetch posts from database
            ids.into_iter()
                .map(|id| GraphqlPost {
                    id,
                    title: format!("Post {}", id),
                    content: format!("Content of post {}", id),
                    author_id: 1,
                    published: true,
                    created_at: chrono::Utc::now().to_rfc3339(),
                })
                .collect()
        })
    });
    
    let context = OptimizedContext {
        user_loader,
        post_loader,
    };
    
    OptimizedSchema::new(
        OptimizedQueryRoot,
        SecuredMutationRoot,
        SubscriptionRoot,
    )
}

Testing GraphQL

Test your GraphQL endpoints:

use oxidite::prelude::*;
use oxidite_testing::TestServer;

#[cfg(test)]
mod graphql_tests {
    use super::*;
    
    #[tokio::test]
    async fn test_graphql_query() {
        let schema = Arc::new(create_advanced_schema());
        let server = TestServer::new(move |router| {
            router.post("/graphql")
                .with_state(schema.clone())
                .handler(graphql_endpoint);
        }).await;
        
        let query = r#"
        {
            users {
                id
                name
                email
            }
        }
        "#;
        
        let response = server
            .post("/graphql")
            .json(&serde_json::json!({
                "query": query
            }))
            .send()
            .await;
        
        assert_eq!(response.status(), 200);
        
        let json: serde_json::Value = response.json().await;
        assert!(json["data"]["users"].is_array());
    }
    
    #[tokio::test]
    async fn test_graphql_mutation() {
        let schema = Arc::new(create_advanced_schema());
        let server = TestServer::new(move |router| {
            router.post("/graphql")
                .with_state(schema.clone())
                .handler(graphql_endpoint);
        }).await;
        
        let mutation = r#"
        mutation {
            createUser(newUser: {name: "Test User", email: "test@example.com", age: 30}) {
                id
                name
                email
                age
            }
        }
        "#;
        
        let response = server
            .post("/graphql")
            .json(&serde_json::json!({
                "query": mutation
            }))
            .send()
            .await;
        
        assert_eq!(response.status(), 200);
        
        let json: serde_json::Value = response.json().await;
        assert!(json["data"]["createUser"]["id"].is_string());
        assert_eq!(json["data"]["createUser"]["name"], "Test User");
    }
    
    #[tokio::test]
    async fn test_graphql_error_handling() {
        let schema = Arc::new(create_advanced_schema());
        let server = TestServer::new(move |router| {
            router.post("/graphql")
                .with_state(schema.clone())
                .handler(graphql_endpoint);
        }).await;
        
        let invalid_query = r#"
        {
            invalidField
        }
        "#;
        
        let response = server
            .post("/graphql")
            .json(&serde_json::json!({
                "query": invalid_query
            }))
            .send()
            .await;
        
        assert_eq!(response.status(), 200); // GraphQL returns 200 even with errors
        
        let json: serde_json::Value = response.json().await;
        assert!(json["errors"].is_array());
        assert!(json["errors"].as_array().unwrap().len() > 0);
    }
    
    #[tokio::test]
    async fn test_graphql_authentication() {
        // Test authenticated GraphQL endpoint
        let schema = Arc::new(create_secured_schema());
        let server = TestServer::new(move |router| {
            router.post("/graphql")
                .middleware(graphql_auth_middleware)
                .with_state(schema.clone())
                .handler(graphql_endpoint);
        }).await;
        
        let query = r#"
        {
            me {
                id
                name
                email
            }
        }
        "#;
        
        // Request without authentication should fail
        let response = server
            .post("/graphql")
            .json(&serde_json::json!({
                "query": query
            }))
            .send()
            .await;
        
        assert_eq!(response.status(), 200);
        
        let json: serde_json::Value = response.json().await;
        // Should have an error about authentication being required
        if let Some(errors) = json["errors"].as_array() {
            assert!(!errors.is_empty());
        }
        
        // Request with authentication should succeed
        let response = server
            .post("/graphql")
            .header("Authorization", "Bearer valid_token")
            .json(&serde_json::json!({
                "query": query
            }))
            .send()
            .await;
        
        assert_eq!(response.status(), 200);
    }
}

Summary

GraphQL integration in Oxidite provides:

  • Schema Definition: Define types and operations with Rust structs
  • Query and Mutation Support: Handle data fetching and modifications
  • Database Integration: Connect resolvers to your data models
  • Authentication: Secure your GraphQL endpoints
  • Subscriptions: Real-time data updates via WebSockets
  • Performance Optimization: DataLoader pattern and caching
  • Testing: Comprehensive testing utilities
  • Error Handling: Proper GraphQL error responses

GraphQL offers a flexible alternative to REST APIs, allowing clients to request exactly the data they need while maintaining strong typing and introspection capabilities.