reinhardt-graphql 0.3.19

GraphQL API support for Reinhardt (facade crate)
Documentation

reinhardt-graphql

GraphQL integration (facade crate)

Overview

GraphQL API support with schema generation from models, query and mutation resolvers, and integration with the authentication and permission system. Provides a flexible alternative to REST APIs.

This is a facade crate that re-exports functionality from the following modules:

  • core: Core GraphQL implementation
  • macros: Procedural macros for GraphQL (when features enabled)

Crate Structure

reinhardt-graphql/              (facade crate - public API)
└── macros/                     (procedural macros)

Users should depend on `reinhardt-graphql` (this facade crate) for all GraphQL functionality.

Features

Implemented ✓

Core Type System

  • GraphQL Type Markers: GraphQLType and GraphQLField traits for type-safe GraphQL type definitions
  • Error Handling: Custom GraphQLError enum with Schema, Resolver, and NotFound variants
  • Base Resolver Trait: Async Resolver trait with generic output types for flexible resolver implementation

Schema & Data Types

  • User Type: Complete GraphQL object implementation with id, name, email, and active fields
  • User Storage: Thread-safe in-memory storage using Arc<RwLock<HashMap>> for user data
    • new(): Create new storage instance
    • add_user(): Add or update user in storage
    • get_user(): Retrieve user by ID
    • list_users(): List all stored users
  • Input Types: CreateUserInput for user creation mutations
  • Schema Builder: create_schema() function to build GraphQL schema with data context

Query Operations

  • User Queries:
    • user(id: ID): Retrieve single user by ID
    • users(): List all users
    • hello(name: Option<String>): Simple greeting query for testing
  • Context Integration: Queries access UserStorage through GraphQL context

Mutation Operations

  • User Mutations:
    • createUser(input: CreateUserInput): Create new user with auto-generated UUID
    • updateUserStatus(id: ID, active: bool): Update user active status
  • State Management: Mutations persist changes to UserStorage

Subscription System

  • Event Types: UserEvent enum supporting Created, Updated, and Deleted events
  • Event Broadcasting: EventBroadcaster with tokio broadcast channel (capacity: 100)
    • new(): Create new broadcaster instance
    • broadcast(): Send events to all subscribers
    • subscribe(): Subscribe to event stream
  • Subscription Root: SubscriptionRoot with filtered subscription streams
    • userCreated(): Stream of user creation events
    • userUpdated(): Stream of user update events
    • userDeleted(): Stream of user deletion events (returns ID only)
  • Async Streams: Real-time event filtering using async-stream

Integration

  • async-graphql Integration: Built on async-graphql framework for production-ready GraphQL server
  • Type Safety: Full Rust type system integration with compile-time guarantees
  • Async/Await: Complete async support with tokio runtime
  • Documentation: Comprehensive doc comments with examples for all public APIs

gRPC Transport (Optional - graphql-grpc feature)

  • GraphQL over gRPC Service: GraphQLGrpcService implementing gRPC protocol for GraphQL
    • execute_query(): Execute GraphQL queries via unary RPC
    • execute_mutation(): Execute GraphQL mutations via unary RPC
    • execute_subscription(): Execute GraphQL subscriptions via server streaming RPC
  • Operation-Class Enforcement: Each RPC validates the selected operation before execution. Malformed documents, invalid or ambiguous operation selections, and operation-class mismatches return gRPC INVALID_ARGUMENT before any resolver runs. Subscription failures are reported as stream errors. Multiple operations require an exact operation_name; a single operation may omit it or use an empty name.
  • Protocol Buffers: Complete proto definitions in reinhardt-grpc crate
    • GraphQLRequest: query, variables, operation_name
    • GraphQLResponse: data, errors, extensions
    • SubscriptionEvent: id, event_type, payload, timestamp
  • Request/Response Conversion: Automatic conversion between gRPC and async-graphql types
  • Error Handling: Full error information propagation (message, locations, path, extensions)
  • Performance: Minimal overhead (5-21%, or 0.2-0.8 µs) compared to direct execution
  • Network Communication: Full TCP/HTTP2 support via tonic
  • Streaming: Efficient server-side streaming for real-time subscriptions

Dependency Injection (Optional - di feature)

  • #[graphql_handler] macro: Attribute macro for resolvers with automatic dependency injection
  • GraphQLContextExt trait: Extension trait for extracting DI context from GraphQL context
  • SchemaBuilderExt trait: Convenience methods for adding DI context to schema
  • Cache Control: Per-parameter cache control with #[inject(cache = false)]
  • Type Safety: Full compile-time type checking for injected dependencies
  • REST Consistency: Same DI patterns as REST handlers for unified developer experience

Installation

Add reinhardt to your Cargo.toml:

[dependencies]
reinhardt = { version = "0.3.19", features = ["graphql"] }

# Or use a preset:
# reinhardt = { version = "0.3.19", features = ["standard"] }  # Recommended
# reinhardt = { version = "0.3.19", features = ["full"] }      # All features

Then import GraphQL features:

use reinhardt::graphql::{Schema, Query, Mutation};
use reinhardt::graphql::types::{UserStorage, UserEvent};

Note: GraphQL features are included in the standard and full feature presets.

Optional Features

# With dependency injection
reinhardt = { version = "0.3.19", features = ["graphql", "di"] }

# With gRPC transport
reinhardt = { version = "0.3.19", features = ["graphql", "grpc"] }

Examples

Basic GraphQL Usage

use async_graphql::{EmptySubscription, Schema};
use reinhardt::graphql::schema::{Mutation, Query, UserStorage};

#[tokio::main]
async fn main() {
    let storage = UserStorage::new();
    let schema = Schema::build(Query, Mutation, EmptySubscription)
        .data(storage)
        .finish();

    let query = r#"{ hello(name: "World") }"#;
    let result = schema.execute(query).await;
    println!("{}", result.data);
}

Dependency Injection

Enable the di feature to use dependency injection in GraphQL resolvers:

use async_graphql::{Context, Object, Result, ID, Schema, EmptyMutation, EmptySubscription};
use reinhardt::graphql::{graphql_handler, SchemaBuilderExt};
use reinhardt_di::InjectionContext;
use std::sync::Arc;

// Define your resolvers
pub struct Query;

#[Object]
impl Query {
    async fn user(&self, ctx: &Context<'_>, id: ID) -> Result<User> {
        user_impl(ctx, id).await
    }

    async fn users(&self, ctx: &Context<'_>, limit: Option<i32>) -> Result<Vec<User>> {
        users_impl(ctx, limit).await
    }
}

// Use #[graphql_handler] for DI
#[graphql_handler]
async fn user_impl(
    ctx: &Context<'_>,
    id: ID,
    #[inject] db: DatabaseConnection,  // Auto-injected
    #[inject] cache: RedisCache,       // Auto-injected
) -> Result<User> {
    // Check cache first
    if let Some(user) = cache.get(&id).await {
        return Ok(user);
    }

    // Fetch from database
    let user = db.fetch_user(&id).await?;

    // Update cache
    cache.set(&id, &user).await;

    Ok(user)
}

#[graphql_handler]
async fn users_impl(
    ctx: &Context<'_>,
    limit: Option<i32>,
    #[inject] db: DatabaseConnection,
) -> Result<Vec<User>> {
    let limit = limit.unwrap_or(100);
    let users = db.fetch_users(limit).await?;
    Ok(users)
}

// Build schema with DI context
#[tokio::main]
async fn main() {
    let injection_ctx = Arc::new(InjectionContext::new());

    // Register dependencies
    // injection_ctx.register(DatabaseConnection::new(...));
    // injection_ctx.register(RedisCache::new(...));

    let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
        .with_di_context(injection_ctx)  // Helper method from SchemaBuilderExt
        .finish();

    // Execute query
    let query = r#"{ user(id: "123") { id name email } }"#;
    let result = schema.execute(query).await;
    println!("{}", result.data);
}

Cache Control

Control dependency caching per parameter:

#[graphql_handler]
async fn handler(
    ctx: &Context<'_>,
    id: ID,
    #[inject] cached_db: DatabaseConnection,          // Cached (default)
    #[inject(cache = false)] fresh_db: DatabaseConnection,  // Not cached
) -> Result<User> {
    // ...
}

GraphQL over gRPC Server

0.3.17 security release: Custom schema construction and subscription error delivery change in this release under a scoped patch compatibility exception. Read the 0.3.17 migration guide before upgrading.

Migration: Replace Schema::build(...) or Schema::new(...) with GraphQLGrpcService::schema_builder(...).finish() (keep existing builder calls before finish). GraphQLGrpcService::new rejects schemas without the guard at construction time. The built-in create_schema helpers install it automatically when graphql-grpc is enabled.

The guard runs after all prepare_request and parse_query extensions. Request rewrites, persisted documents, and parsing limits therefore retain their normal order, while the final selected operation must match the RPC. Schema validation and resolver authorization still apply. The finished schema can also serve HTTP requests without a gRPC operation restriction.

use async_graphql::EmptySubscription;
use reinhardt::graphql::grpc_service::GraphQLGrpcService;
use reinhardt::graphql::schema::{Mutation, Query, UserStorage};
use reinhardt::grpc::proto::graphql::graph_ql_service_server::GraphQlServiceServer;
use tonic::transport::Server;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let storage = UserStorage::new();
    let schema = GraphQLGrpcService::schema_builder(Query, Mutation, EmptySubscription)
        .data(storage)
        .finish();

    let service = GraphQLGrpcService::new(schema);
    let grpc_service = GraphQlServiceServer::new(service);

    Server::builder()
        .add_service(grpc_service)
        .serve("127.0.0.1:50051".parse()?)
        .await?;

    Ok(())
}

GraphQL over gRPC Client

use reinhardt::grpc::proto::graphql::{
    graph_ql_service_client::GraphQlServiceClient,
    GraphQlRequest,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = GraphQlServiceClient::connect("http://127.0.0.1:50051").await?;

    let request = tonic::Request::new(GraphQlRequest {
        query: r#"{ hello(name: "gRPC") }"#.to_string(),
        variables: None,
        operation_name: None,
    });

    let response = client.execute_query(request).await?;
    println!("{:?}", response.into_inner());

    Ok(())
}

Running Examples

# Start gRPC server
cargo run --package reinhardt-graphql --features graphql-grpc --example grpc_server

# In another terminal, run client
cargo run --package reinhardt-graphql --features graphql-grpc --example grpc_client

Testing

# All tests
cargo test --package reinhardt-graphql --features graphql-grpc

# Integration tests
cargo test --package reinhardt-graphql --features graphql-grpc --test grpc_integration_tests

# Subscription streaming tests
cargo test --package reinhardt-graphql --features graphql-grpc --test grpc_subscription_tests

# E2E tests with real network
cargo test --package reinhardt-graphql --features graphql-grpc --test grpc_e2e_tests

# Performance benchmarks
cargo bench --package reinhardt-graphql --features graphql-grpc

Performance

See PERFORMANCE.md for detailed benchmarks.

Summary:

  • Direct GraphQL: ~3-4 µs per query
  • gRPC GraphQL: ~4-5 µs per query
  • Overhead: 5-21% (+0.2-0.8 µs) for gRPC serialization
  • Both approaches are highly performant for real-world applications