this

GraphQL Implementation Architecture

This document explains the technical implementation of GraphQL exposure in this-rs, including the dynamic schema generation and custom executor.

๐Ÿ“ Overview

The GraphQL implementation is completely modular and separate from the core framework:

src/server/exposure/
โ”œโ”€โ”€ rest/
โ”‚   โ””โ”€โ”€ mod.rs         # REST API exposure
โ””โ”€โ”€ graphql/
    โ”œโ”€โ”€ mod.rs         # GraphQL exposure entry point
    โ”œโ”€โ”€ schema.rs      # Legacy async-graphql schema (unused)
    โ”œโ”€โ”€ schema_generator.rs  # Dynamic SDL schema generator
    โ”œโ”€โ”€ dynamic_schema.rs    # Legacy dynamic schema (unused)
    โ””โ”€โ”€ executor/
        โ”œโ”€โ”€ mod.rs     # Executor module entry
        โ”œโ”€โ”€ core.rs    # Main executor orchestration
        โ”œโ”€โ”€ query_executor.rs    # Query resolution
        โ”œโ”€โ”€ mutation_executor.rs # CRUD mutations
        โ”œโ”€โ”€ link_mutations.rs    # Link-specific mutations
        โ”œโ”€โ”€ field_resolver.rs    # Field and relation resolution
        โ””โ”€โ”€ utils.rs   # Utility functions

๐Ÿ—๏ธ Architecture Components

1. GraphQLExposure

Location: src/server/exposure/graphql/mod.rs

The entry point that builds the GraphQL router. Itโ€™s completely separate from REST exposure.

pub struct GraphQLExposure;

impl GraphQLExposure {
    pub fn build_router(host: Arc<ServerHost>) -> Result<Router> {
        Router::new()
            .route("/graphql", post(graphql_handler_custom))
            .route("/graphql/playground", get(graphql_playground))
            .route("/graphql/schema", get(graphql_dynamic_schema))
            .layer(Extension(host))
    }
}

Endpoints:

2. SchemaGenerator

Location: src/server/exposure/graphql/schema_generator.rs

Dynamically generates GraphQL SDL (Schema Definition Language) from:

Key Methods:

Example Output:

type Order {
  id: ID!
  number: String!
  customerName: String!
  amount: Float!
  invoices: [Invoice!]!  # From links.yaml
}

type Query {
  order(id: ID!): Order
  orders(limit: Int, offset: Int): [Order!]!
}

type Mutation {
  createOrder(data: JSON!): Order!
  updateOrder(id: ID!, data: JSON!): Order!
  deleteOrder(id: ID!): Boolean!
  createInvoiceForOrder(parentId: ID!, data: JSON!): Invoice!
}

3. GraphQLExecutor

Location: src/server/exposure/graphql/executor/core.rs

A custom GraphQL executor that:

Why Custom?: async-graphql requires compile-time type definitions. Our dynamic schema requires runtime execution, so we implemented a custom executor.

Structure:

pub struct GraphQLExecutor {
    host: Arc<ServerHost>,
    schema_sdl: String,  // Generated SDL (currently unused but stored)
}

impl GraphQLExecutor {
    pub async fn execute(&self, query: &str, variables: Option<HashMap<String, Value>>) -> Result<Value>;
    async fn execute_document(&self, doc: &Document, variables: HashMap<String, Value>) -> Result<Value>;
    async fn execute_query(&self, selections: &[Selection], variables: &HashMap<String, Value>) -> Result<Value>;
    async fn execute_mutation(&self, selections: &[Selection], variables: &HashMap<String, Value>) -> Result<Value>;
}

4. Query Executor

Location: src/server/exposure/graphql/executor/query_executor.rs

Resolves GraphQL queries:

pub async fn resolve_query_field(
    host: &Arc<ServerHost>,
    field: &Field<'_, String>,
) -> Result<Value> {
    // Check if plural query
    if let Some(entity_type) = get_entity_type_from_plural(host, field_name) {
        let entities = fetcher.list_as_json(limit, offset).await?;
        // Resolve sub-fields...
    }
    
    // Check if singular query
    if let Some(entity_type) = get_entity_type_from_singular(host, field_name) {
        let entity = fetcher.fetch_as_json(&uuid).await?;
        // Resolve sub-fields...
    }
}

5. Mutation Executor

Location: src/server/exposure/graphql/executor/mutation_executor.rs

Handles all CRUD mutations:

Dispatches to specialized modules for link mutations.

Location: src/server/exposure/graphql/executor/link_mutations.rs

Specialized mutations for link management:

Convention: Mutation names follow patterns:

7. Field Resolver

Location: src/server/exposure/graphql/executor/field_resolver.rs

Resolves entity fields and relations:

Key Functions:

pub async fn resolve_entity_fields(
    host: &Arc<ServerHost>,
    entity: Value,
    selections: &[Selection],
    entity_type: &str,
) -> Result<Value>

async fn resolve_relation_field_inner(
    host: &Arc<ServerHost>,
    entity: &serde_json::Map<String, Value>,
    field: &Field,
    entity_type: &str,
) -> Result<Option<Value>>

Relation Resolution Logic:

  1. Check if field name matches forward_route_name in links config โ†’ Forward relation
  2. Check if field name matches reverse_route_name in links config โ†’ Reverse relation
  3. Use LinkService::find_by_source() or find_by_target() to get links
  4. Fetch linked entities via EntityFetcher::fetch_as_json()
  5. Recursively resolve nested selections

๐Ÿ”„ Execution Flow

Query Execution

1. HTTP Request โ†’ POST /graphql
   โ†“
2. graphql_handler_custom() โ†’ Creates GraphQLExecutor
   โ†“
3. GraphQLExecutor::execute()
   โ†“ Parse query with graphql-parser
   โ†“
4. execute_document() โ†’ Detect operation type
   โ†“
5. execute_query() โ†’ For each field
   โ†“
6. query_executor::resolve_query_field()
   โ†“ Identify entity type (plural/singular)
   โ†“
7. EntityFetcher::list_as_json() or fetch_as_json()
   โ†“
8. field_resolver::resolve_entity_fields()
   โ†“ For each selection
   โ†“
9a. Direct field โ†’ Extract from JSON
9b. Relation field โ†’ resolve_relation_field_inner()
   โ†“
10. LinkService::find_by_source() / find_by_target()
    โ†“
11. EntityFetcher::fetch_as_json() for each linked entity
    โ†“
12. Recursive resolve_entity_fields() for nested selections
    โ†“
13. Return resolved JSON

Mutation Execution

1. HTTP Request โ†’ POST /graphql (mutation)
   โ†“
2. GraphQLExecutor::execute()
   โ†“
3. execute_mutation() โ†’ For each field
   โ†“
4. mutation_executor::resolve_mutation_field()
   โ†“ Dispatch by mutation name pattern
   โ†“
5a. CRUD mutation โ†’ mutation_executor::create/update/delete_entity_mutation()
    โ†“ EntityCreator::create_from_json() / update_from_json() / delete()
    
5b. Link mutation โ†’ link_mutations::*_mutation()
    โ†“ LinkService::create() / delete()
    โ†“ LinkService::find_by_source() (for unlink)
    
6. field_resolver::resolve_entity_fields() โ†’ Resolve sub-selections
   โ†“
7. Return resolved entity/link

๐ŸŽฏ Design Decisions

Why Custom Executor Instead of async-graphql?

Problem: async-graphql requires compile-time type definitions. Our entities are defined at runtime.

Solution: Custom executor that:

Trade-offs:

Why JSON for Mutation Data?

Decision: Use JSON! scalar for mutation data argument instead of strongly-typed input types.

Rationale:

Trade-offs:

Why Separate Executor Modules?

Decision: Split executor into 6 modules (core, query, mutation, links, fields, utils).

Rationale:

Structure:

๐Ÿ”ง Extension Points

Adding New Query Types

  1. Add query to SchemaGenerator::generate_query_root()
  2. Add resolver in query_executor.rs

Adding New Mutation Types

  1. Add mutation to SchemaGenerator::generate_mutation_root()
  2. Add handler in mutation_executor.rs or link_mutations.rs

Adding New Field Resolvers

  1. Extend field_resolver.rs with new resolution logic
  2. Update resolve_entity_fields_impl() to handle new field types

๐Ÿ“Š Performance Considerations

Schema Generation

Current: Schema is generated on each request to /graphql/schema.

Future Optimization: Cache generated SDL and invalidate when entities change.

Query Execution

Current: Executor created per request.

Future Optimization: Cache executor instance (schema doesnโ€™t change at runtime).

Field Resolution

Current: Sequential fetching of related entities.

Future Optimization: Batch fetching using DataLoader pattern.

Nested Queries

Current: Recursive resolution may fetch same entity multiple times.

Future Optimization: Add query depth limit and entity fetching cache.

๐Ÿงช Testing

Each executor module can be tested independently:

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_utils_pluralize() {
        assert_eq!(utils::pluralize("order"), "orders");
        assert_eq!(utils::pluralize("company"), "companies");
    }
    
    #[tokio::test]
    async fn test_query_resolution() {
        let host = create_test_host();
        let field = parse_query_field("orders");
        let result = query_executor::resolve_query_field(&host, &field).await?;
        // Assert result...
    }
}

๐Ÿ”ฎ Future Enhancements

Planned Features

  1. Schema Caching: Cache generated SDL
  2. Executor Caching: Reuse executor instance
  3. DataLoader: Batch entity fetching
  4. Query Complexity Analysis: Prevent expensive queries
  5. Field-Level Authorization: Integrate with auth system
  6. Subscriptions: WebSocket support for real-time updates
  7. Directives Support: @deprecated, @skip, @include
  8. Input Type Generation: Strongly-typed input types (if possible)

Technical Debt

  1. Legacy Files: schema.rs and dynamic_schema.rs are unused (can be removed)
  2. Error Handling: More structured GraphQL errors
  3. Performance: Add benchmarks and optimize hot paths
  4. Documentation: Add inline documentation for complex logic

๐ŸŽฏ Summary

The GraphQL implementation is:

Key Innovation: Custom executor that enables truly dynamic GraphQL without sacrificing type safety or developer experience.