This guide explains GraphQL mutations with detailed visual diagrams showing exactly how our server processes requests from start to finish. Perfect for developers new to GraphQL or those wanting to understand the internal mechanics.
GraphQL has two main operation types for different purposes:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ GRAPHQL OPERATIONS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ๐ QUERIES (Read Data) ๐ง MUTATIONS (Modify Data) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Purpose: Fetch data โ โ Purpose: Change data โ โ
โ โ Side Effects: None โ โ Side Effects: Yes โ โ
โ โ Execution: Parallel OK โ โ Execution: Sequential ONLY โ โ
โ โ Caching: Safe โ โ Caching: Dangerous โ โ
โ โ Idempotent: Yes โ โ Idempotent: No โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Example: Example: โ
โ query GetUser { mutation CreateUser { โ
โ user(id: "123") { createUser(input: { โ
โ name name: "Alice" โ
โ email email: "alice@example.com" โ
โ } }) { โ
โ } id name email โ
โ } โ
โ } โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
mutation
, you know data will be modifiedLetโs trace exactly what happens when a mutation request hits our server:
๐ก INCOMING HTTP REQUEST
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ HTTP LAYER โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ POST /graphql โ
โ Content-Type: application/json โ
โ { โ
โ "query": "mutation { createUser(input: { name: \"Alice\" }) { id name } }", โ
โ "variables": {}, โ
โ "operationName": null โ
โ } โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ LEXER (Token Analysis) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Raw String: "mutation { createUser(input: { name: \"Alice\" }) { id name } }" โ
โ โ Break into tokens โ
โ Tokens: [MUTATION, LBRACE, IDENTIFIER("createUser"), LPAREN, ...] โ
โ โ
โ ๐ Located in: src/infrastructure/lexer.rs โ
โ ๐ง Key Functions: tokenize(), process_identifier(), process_string() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐๏ธ PARSER (AST Generation) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Tokens: [MUTATION, LBRACE, IDENTIFIER("createUser"), ...] โ
โ โ Build Abstract Syntax Tree โ
โ AST: โ
โ Document { โ
โ definitions: [ โ
โ OperationDefinition { โ
โ operation_type: Mutation, โ
โ selection_set: SelectionSet { โ
โ selections: [ โ
โ Field { โ
โ name: "createUser", โ
โ arguments: [ โ
โ Argument { name: "input", value: Object(...) } โ
โ ] โ
โ } โ
โ ] โ
โ } โ
โ } โ
โ ] โ
โ } โ
โ โ
โ ๐ Located in: src/infrastructure/query_parser.rs โ
โ ๐ง Key Functions: parse_document(), parse_operation_definition() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
VALIDATION (Schema Check) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ AST + Schema โ Validation Rules โ
โ โ
โ โ Does Mutation type exist in schema? โ
โ โ Does createUser field exist on Mutation type? โ
โ โ Are argument types correct? โ
โ โ Are requested fields available on return type? โ
โ โ Are all required fields provided? โ
โ โ
โ ๐ Located in: src/domain/services.rs (QueryValidator) โ
โ ๐ง Key Functions: validate(), check_field_existence() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โก EXECUTION ENGINE โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๐ MUTATION-SPECIFIC PROCESSING โ
โ โ
โ 1๏ธโฃ Identify Operation Type: MUTATION โ
โ โ โ
โ 2๏ธโฃ Get Mutation Root Type from Schema โ
โ schema.mutation_type โ "Mutation" โ
โ โ โ
โ 3๏ธโฃ โ ๏ธ SEQUENTIAL EXECUTION (Critical!) โ
โ Unlike queries, mutations MUST execute one-by-one: โ
โ โ
โ for field in selection_set { // โ Sequential loop, NOT parallel! โ
โ result = execute_mutation_field(field).await; โ
โ // โ๏ธ Wait for completion before next field โ
โ } โ
โ โ
โ ๐ Located in: src/domain/services.rs (QueryExecutor) โ
โ ๐ง Key Functions: execute_mutation_operation() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ฏ FIELD RESOLUTION โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ createUser Field Execution โ
โ โ
โ 1๏ธโฃ Find Field Definition: โ
โ Mutation.createUser: (input: CreateUserInput!) โ User! โ
โ โ
โ 2๏ธโฃ Extract Arguments: โ
โ input = { name: "Alice", email: "alice@example.com" } โ
โ โ
โ 3๏ธโฃ Execute Resolver Logic: ๐ฅ SIDE EFFECTS HAPPEN HERE! โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ // This is where real-world resolvers would: โ โ
โ โ // - Validate input data โ โ
โ โ // - Write to database โ โ
โ โ // - Call external APIs โ โ
โ โ // - Generate IDs and timestamps โ โ
โ โ // - Send notifications โ โ
โ โ โ โ
โ โ let user = database.create_user({ โ โ
โ โ name: input.name, โ โ
โ โ email: input.email, โ โ
โ โ id: generate_uuid(), โ โ
โ โ created_at: now() โ โ
โ โ }); โ โ
โ โ return user; โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ 4๏ธโฃ Process Sub-Selection: โ
โ User { id name } โ Extract only requested fields โ
โ โ
โ ๐ Located in: src/domain/services.rs โ
โ ๐ง Key Functions: execute_mutation_field(), execute_mutation_sub_selection() โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ฆ RESPONSE CONSTRUCTION โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Field Results โ JSON Response โ
โ โ
โ createUser result: { โ
โ "id": "user_abc123", โ
โ "name": "Alice" โ
โ } โ
โ โ Wrap in GraphQL response format โ
โ { โ
โ "data": { โ
โ "createUser": { โ
โ "id": "user_abc123", โ
โ "name": "Alice" โ
โ } โ
โ }, โ
โ "errors": [], โ
โ "extensions": {} โ
โ } โ
โ โ
โ ๐ Located in: src/domain/value_objects.rs (ExecutionResult) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
๐ค HTTP RESPONSE SENT TO CLIENT
This is the most important concept in GraphQL mutations:
โ WRONG - Parallel Execution (What Queries Do)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ mutation { โ
โ first: createUser(name: "Alice") โโโโฌโโบ Database โ
โ second: createUser(name: "Bob") โโโโค โ
โ third: createUser(name: "Charlie") โโโโ โ
โ } โ
โ โ
โ โ ๏ธ RACE CONDITION! All execute at once โ
โ โ ๏ธ Unpredictable order โ
โ โ ๏ธ Data corruption possible โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
CORRECT - Sequential Execution (What Mutations Do)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ mutation { โ
โ first: createUser(name: "Alice") โโ1โโบ Database โ
โ โ โ
โ โผ (wait for completion) โ
โ second: createUser(name: "Bob") โโ2โโบ Database โ
โ โ โ
โ โผ (wait for completion) โ
โ third: createUser(name: "Charlie") โโ3โโบ Database โ
โ } โ
โ โ
โ โ
Predictable order (1, 2, 3) โ
โ โ
Each sees previous results โ
โ โ
Data consistency guaranteed โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
mutation TransferMoney {
# These MUST execute in order!
debit: updateAccount(id: "alice", amount: -100) # 1st: Remove money from Alice
credit: updateAccount(id: "bob", amount: 100) # 2nd: Add money to Bob
log: createTransaction(from: "alice", to: "bob") # 3rd: Record the transaction
}
What happens if they run in parallel? ๐ฅ
Sequential execution prevents this โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ OUR GRAPHQL SERVER ARCHITECTURE โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ๐ HTTP Layer (Axum) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ POST /graphql โ โ
โ โ โโโบ Extract JSON body โ โ
โ โ โโโบ Forward to Application Layer โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ ๐ฏ Application Layer (Use Cases) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ GraphQLUseCase::execute() โ โ
โ โ โโโบ Parse query string โ โ
โ โ โโโบ Validate against schema โ โ
โ โ โโโบ Delegate to Domain Services โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ ๐ง Domain Layer (Core Logic) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ QueryExecutor::execute() โ โ
โ โ โโโบ Detect operation type โ โ
โ โ โโโบ Route to appropriate executor: โ โ
โ โ โ โโโบ execute_query_operation() (parallel) โ โ
โ โ โ โโโบ execute_mutation_operation() (sequential) โโโโโ YOU ARE HERE โ
โ โ โโโบ Return ExecutionResult โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ โ
โ โ๏ธ Infrastructure Layer (Parsing, Storage) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ QueryParser, Lexer, Schema Repository โ โ
โ โ โโโบ Convert strings to AST โ โ
โ โ โโโบ Manage schema definitions โ โ
โ โ โโโบ Future: Database connections, external APIs โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Now letโs dive into the actual Rust code that makes this work:
// src/domain/services.rs - The heart of mutation processing
impl QueryExecutor {
/// Main entry point for executing any GraphQL operation
async fn execute(&self, query: &Query, schema: &Schema) -> ExecutionResult {
// 1. Parse the query string into AST
let document = self.parse_query(query.query_string())?;
// 2. Find the operation to execute
let operation = self.find_operation(&document, None)?;
// 3. Route based on operation type
match operation.operation_type {
OperationType::Query => {
// Queries can execute fields in parallel
self.execute_query_operation(operation, schema, variables).await
}
OperationType::Mutation => {
// ๐จ Mutations MUST execute sequentially!
self.execute_mutation_operation(operation, schema, variables).await
}
OperationType::Subscription => {
// Future: Real-time subscriptions
Err(GraphQLError::new("Subscriptions not yet implemented"))
}
}
}
/// Execute mutation with sequential field processing
async fn execute_mutation_operation(
&self,
operation: &OperationDefinition,
schema: &Schema,
variables: &Option<serde_json::Value>,
) -> Result<serde_json::Value, GraphQLError> {
// Get the Mutation root type
let mutation_type_name = schema.mutation_type
.as_ref()
.ok_or_else(|| GraphQLError::new("No Mutation type defined"))?;
let mutation_type = schema.get_type(mutation_type_name)
.ok_or_else(|| GraphQLError::new("Mutation type not found"))?;
// ๐ฅ THE CRITICAL PART: Sequential execution
self.execute_mutation_selection_set_sequential(
&operation.selection_set,
mutation_type,
variables,
).await
}
/// Execute mutation fields one-by-one (NOT parallel!)
async fn execute_mutation_selection_set_sequential(
&self,
selection_set: &SelectionSet,
mutation_type: &GraphQLType,
variables: &Option<serde_json::Value>,
) -> Result<serde_json::Value, GraphQLError> {
let mut result_map = serde_json::Map::new();
// ๐จ SEQUENTIAL LOOP - Each field waits for previous to complete
for selection in &selection_set.selections {
match selection {
Selection::Field(field) => {
// Execute this field and WAIT for completion
let field_result = self.execute_mutation_field(field, mutation_type).await?;
// Add to result map
let result_key = field.alias.as_ref().unwrap_or(&field.name);
result_map.insert(result_key.clone(), field_result);
// โ๏ธ Only now do we move to the next field!
}
// Handle fragments, inline fragments, etc.
_ => { /* ... */ }
}
}
Ok(serde_json::Value::Object(result_map))
}
/// Execute individual mutation field with side effects
async fn execute_mutation_field(
&self,
field: &Field,
mutation_type: &GraphQLType,
) -> Result<serde_json::Value, GraphQLError> {
// This is where the actual business logic happens!
match field.name.as_str() {
"createUser" => {
// ๐ฅ SIDE EFFECTS: Create user in database
let input = self.extract_arguments(&field.arguments);
let user = self.user_service.create_user(input).await?;
// Return the created user data
Ok(serde_json::to_value(user)?)
}
"updateUser" => {
// ๐ฅ SIDE EFFECTS: Update user in database
let id = self.get_argument_value(&field.arguments, "id")?;
let input = self.get_argument_value(&field.arguments, "input")?;
let user = self.user_service.update_user(id, input).await?;
Ok(serde_json::to_value(user)?)
}
"deleteUser" => {
// ๐ฅ SIDE EFFECTS: Delete user from database
let id = self.get_argument_value(&field.arguments, "id")?;
let success = self.user_service.delete_user(id).await?;
Ok(serde_json::Value::Bool(success))
}
_ => {
Err(GraphQLError::new(format!("Unknown mutation field: {}", field.name)))
}
}
}
}
Letโs trace through a complete mutation request:
1. Client Request:
POST /graphql
{
"query": "mutation { createUser(input: { name: \"Alice\", email: \"alice@example.com\" }) { id name email createdAt } }"
}
2. Server Processing:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STEP-BY-STEP EXECUTION โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ๐ STEP 1: Lexical Analysis โ
โ Input: "mutation { createUser(input: { name: \"Alice\" }) { id name } }" โ
โ Output: [MUTATION, LBRACE, IDENTIFIER("createUser"), LPAREN, ...] โ
โ โ
โ ๐๏ธ STEP 2: Syntax Parsing โ
โ Tokens โ AST โ
โ OperationDefinition { โ
โ operation_type: Mutation, โ
โ selection_set: SelectionSet { โ
โ selections: [Field("createUser")] โ
โ } โ
โ } โ
โ โ
โ โ
STEP 3: Validation โ
โ โ Mutation type exists in schema โ
โ โ createUser field exists on Mutation type โ
โ โ Arguments match field definition โ
โ โ Return type selection is valid โ
โ โ
โ โก STEP 4: Execution โ
โ execute_mutation_operation() โ
โ โโโบ execute_mutation_selection_set_sequential() โ
โ โโโบ execute_mutation_field("createUser") โ
โ โโโบ // Side effects happen here! โ
โ user_service.create_user({ โ
โ name: "Alice", โ
โ email: "alice@example.com" โ
โ }) โ
โ โโโบ Database: INSERT INTO users ... โ
โ โโโบ Result: User { id: "abc123", name: "Alice", ... } โ
โ โ
โ ๐ฆ STEP 5: Response Construction โ
โ { โ
โ "data": { โ
โ "createUser": { โ
โ "id": "abc123", โ
โ "name": "Alice", โ
โ "email": "alice@example.com", โ
โ "createdAt": "2024-01-15T10:30:00Z" โ
โ } โ
โ } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Client Request:
mutation ComplexTransaction {
# These execute in EXACT order:
first: createUser(input: { name: "Alice", email: "alice@example.com" }) {
id
name
}
second: createUser(input: { name: "Bob", email: "bob@example.com" }) {
id
name
}
third: createProject(input: { name: "Awesome Project", ownerId: "???" }) {
id
name
owner { name }
}
}
Execution Timeline:
TIME: 0ms โ ๐ START mutation execution
โ
TIME: 0ms โ โณ Execute first: createUser (Alice)
โ โโโบ Database query: INSERT INTO users (name, email) VALUES ('Alice', 'alice@...')
TIME: 150ms โ โ
first completed: { id: "user_1", name: "Alice" }
โ
TIME: 150ms โ โณ Execute second: createUser (Bob)
โ โโโบ Database query: INSERT INTO users (name, email) VALUES ('Bob', 'bob@...')
TIME: 300ms โ โ
second completed: { id: "user_2", name: "Bob" }
โ
TIME: 300ms โ โณ Execute third: createProject
โ โโโบ Could reference results from first/second mutations!
โ โโโบ Database query: INSERT INTO projects (name, owner_id) VALUES ('Awesome Project', 'user_1')
TIME: 450ms โ โ
third completed: { id: "proj_1", name: "Awesome Project", owner: { name: "Alice" }}
โ
TIME: 450ms โ ๐ ALL MUTATIONS COMPLETED - Return combined result
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ERROR HANDLING MAP โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โ PARSING ERRORS โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Invalid Syntax: mutation { createUser( missing closing } โ โ
โ โ Result: Parse error before execution starts โ โ
โ โ HTTP Status: 400 Bad Request โ โ
โ โ Response: { "errors": [{ "message": "Syntax error..." }] } โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โ VALIDATION ERRORS โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Missing Field: mutation { nonExistentField } โ โ
โ โ Wrong Arguments: createUser(wrongArg: "value") โ โ
โ โ Type Mismatch: createUser(input: "should be object") โ โ
โ โ Result: Validation error before execution starts โ โ
โ โ HTTP Status: 400 Bad Request โ โ
โ โ Response: { "errors": [{ "message": "Field not found" }] } โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โ EXECUTION ERRORS โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Database Connection Failed โ โ
โ โ Business Logic Error (duplicate email) โ โ
โ โ Permission Denied โ โ
โ โ External API Timeout โ โ
โ โ Result: Partial execution, detailed error info โ โ
โ โ HTTP Status: 200 OK (GraphQL convention) โ โ
โ โ Response: { "data": null, "errors": [...] } โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โ SEQUENTIAL EXECUTION ERRORS โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Scenario: 3 mutations, 2nd one fails โ โ
โ โ โ โ
โ โ mutation { โ โ
โ โ first: createUser(...) โ
Succeeds โ โ
โ โ second: updateUser(...) โ Fails (user not found) โ โ
โ โ third: deleteUser(...) ๐ซ NOT EXECUTED โ โ
โ โ } โ โ
โ โ โ โ
โ โ Result: { "data": { "first": {...}, "second": null }, โ โ
โ โ "errors": [{ "path": ["second"], ... }] } โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// src/domain/services.rs - Error handling in mutation execution
impl QueryExecutor {
async fn execute_mutation_selection_set_sequential(
&self,
selection_set: &SelectionSet,
mutation_type: &GraphQLType,
variables: &Option<serde_json::Value>,
) -> Result<serde_json::Value, GraphQLError> {
let mut result_map = serde_json::Map::new();
let mut errors = Vec::new();
// Execute each field sequentially
for selection in &selection_set.selections {
match selection {
Selection::Field(field) => {
match self.execute_mutation_field(field, mutation_type).await {
Ok(field_result) => {
// Success: Add to result
let result_key = field.alias.as_ref().unwrap_or(&field.name);
result_map.insert(result_key.clone(), field_result);
}
Err(error) => {
// Failure: Record error and STOP execution
errors.push(error);
// ๐จ CRITICAL: Stop processing remaining fields
// This maintains consistency - if one fails, don't continue
break;
}
}
}
}
}
if errors.is_empty() {
Ok(serde_json::Value::Object(result_map))
} else {
// Return partial results + errors (GraphQL convention)
Err(errors.into_iter().next().unwrap()) // Return first error for now
}
}
}
Our lexer and parser already support mutation syntax:
// In infrastructure/lexer.rs
#[token("mutation")]
Mutation,
// In infrastructure/query_parser.rs
#[derive(Debug, Clone, PartialEq)]
pub enum OperationType {
Query,
Mutation, // โ
Already supported
Subscription,
}
The main work is in domain/services.rs
:
impl QueryExecutor {
/// Execute a mutation operation
fn execute_mutation_operation(
&self,
operation: &OperationDefinition,
schema: &Schema,
variables: &Option<serde_json::Value>,
) -> Result<serde_json::Value, GraphQLError> {
// ๐ฏ This is what we're implementing!
// 1. Get the Mutation root type from schema
// 2. Execute selection set sequentially (not parallel!)
// 3. Apply side effects for each field
// 4. Return results
}
}
Unlike query resolvers (which just return data), mutation resolvers have side effects:
pub trait MutationResolver {
/// Apply side effects and return result
async fn resolve(
&self,
field: &str,
args: &HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value, GraphQLError>;
}
## ๐ Getting Started: Your First Mutation
### Setting Up a Schema with Mutations
```rust
// src/main.rs - Setting up a schema with mutations
use graphql_rs::domain::entities::{Schema, types::*};
fn create_schema_with_mutations() -> Schema {
let mut schema = Schema::new("Query".to_string());
// 1. Define the Mutation root type
schema.mutation_type = Some("Mutation".to_string());
// 2. Create User type (return type for mutations)
let user_type = ObjectType {
name: "User".to_string(),
fields: HashMap::from([
("id".to_string(), FieldDefinition {
name: "id".to_string(),
field_type: GraphQLType::Scalar(ScalarType::ID),
// ... other properties
}),
("name".to_string(), FieldDefinition {
name: "name".to_string(),
field_type: GraphQLType::Scalar(ScalarType::String),
// ... other properties
}),
("email".to_string(), FieldDefinition {
name: "email".to_string(),
field_type: GraphQLType::Scalar(ScalarType::String),
// ... other properties
}),
]),
// ... other properties
};
// 3. Create Mutation type with CRUD operations
let mutation_type = ObjectType {
name: "Mutation".to_string(),
fields: HashMap::from([
("createUser".to_string(), FieldDefinition {
name: "createUser".to_string(),
field_type: GraphQLType::Object(user_type.clone()),
arguments: HashMap::from([
("input".to_string(), ArgumentDefinition {
name: "input".to_string(),
arg_type: GraphQLType::InputObject(/* CreateUserInput */),
// ... other properties
}),
]),
// ... other properties
}),
("updateUser".to_string(), FieldDefinition {
name: "updateUser".to_string(),
field_type: GraphQLType::Object(user_type.clone()),
// ... similar structure
}),
("deleteUser".to_string(), FieldDefinition {
name: "deleteUser".to_string(),
field_type: GraphQLType::Scalar(ScalarType::Boolean),
// ... similar structure
}),
]),
// ... other properties
};
// 4. Add types to schema
schema.add_type(GraphQLType::Object(user_type)).unwrap();
schema.add_type(GraphQLType::Object(mutation_type)).unwrap();
schema
}
// examples/my_first_mutation.rs
use graphql_rs::domain::{
entities::Query,
services::{QueryExecutor, QueryExecution},
value_objects::ValidationResult,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create schema with mutation support
let schema = create_schema_with_mutations();
// 2. Create query executor
let executor = QueryExecutor::new();
// 3. Write a mutation query
let mutation_query = r#"
mutation CreateNewUser {
createUser(input: {
name: "John Doe",
email: "john@example.com"
}) {
id
name
email
createdAt
}
}
"#;
// 4. Execute the mutation
let mut query = Query::new(mutation_query.to_string());
query.mark_validated(ValidationResult::valid()); // Skip validation for now
let result = executor.execute(&query, &schema).await;
// 5. Handle the result
match result.data {
Some(data) => {
println!("โ
Mutation successful!");
println!("Created user: {}", serde_json::to_string_pretty(&data)?);
}
None => {
println!("โ Mutation failed:");
for error in result.errors {
println!(" - {}", error.message);
}
}
}
Ok(())
}
// tests/mutation_tests.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_user_mutation() {
// Setup
let schema = create_test_schema();
let executor = QueryExecutor::new();
let mutation = r#"
mutation {
createUser(input: { name: "Alice", email: "alice@example.com" }) {
id
name
email
}
}
"#;
// Execute
let mut query = Query::new(mutation.to_string());
query.mark_validated(ValidationResult::valid());
let result = executor.execute(&query, &schema).await;
// Assert
assert!(result.errors.is_empty(), "Mutation should not have errors");
assert!(result.data.is_some(), "Mutation should return data");
let data = result.data.unwrap();
let user = &data["createUser"];
assert!(user["id"].as_str().is_some(), "Should have generated ID");
assert_eq!(user["name"].as_str().unwrap(), "Alice");
assert_eq!(user["email"].as_str().unwrap(), "alice@example.com");
}
#[tokio::test]
async fn test_sequential_mutation_execution() {
let schema = create_test_schema();
let executor = QueryExecutor::new();
let mutation = r#"
mutation {
first: createUser(input: { name: "User 1" }) { id name }
second: createUser(input: { name: "User 2" }) { id name }
third: createUser(input: { name: "User 3" }) { id name }
}
"#;
let mut query = Query::new(mutation.to_string());
query.mark_validated(ValidationResult::valid());
let result = executor.execute(&query, &schema).await;
// Verify all mutations executed successfully
assert!(result.errors.is_empty());
let data = result.data.unwrap();
// Verify results are in order
assert!(data["first"]["id"].as_str().is_some());
assert!(data["second"]["id"].as_str().is_some());
assert!(data["third"]["id"].as_str().is_some());
println!("โ
All mutations executed sequentially!");
}
}
// examples/test_mutation_support.rs (already in our codebase!)
cargo run --example test_mutation_support
Output:
๐ Testing GraphQL Mutation Support
=====================================
๐ Test 1: Schema with Mutation Type...
โ
Schema created with Mutation type
๐ Available mutations: createUser, updateUser, deleteUser
๐ค Test 2: Create User Mutation...
๐ค Executing mutation: mutation CreateUser { createUser { id name email } }
โ
Mutation executed successfully!
๐ค Created user:
- ID: user_abc123
- Name: Unknown User (default)
- Email: user@example.com (default)
๐ Test 5: Sequential Mutations (CRITICAL TEST!)...
๐ This test verifies mutations execute one-by-one, not in parallel
๐ค Executing sequential mutations
โ
Sequential mutations executed successfully!
๐ฅ Created users in sequence:
1. Unknown User (ID: user_abc123)
2. Unknown User (ID: user_def456)
3. Unknown User (ID: user_ghi789)
๐ฏ Sequential execution verified!
โ
All mutation tests passed!
๐ GraphQL Mutation Support is working correctly!
Try adding a updateUserEmail
mutation to the schema:
type Mutation {
# Existing mutations...
updateUserEmail(id: ID!, newEmail: String!): User
}
Implementation Challenge:
// In execute_mutation_field method, add:
"updateUserEmail" => {
let user_id = self.get_argument_value(&field.arguments, "id")?;
let new_email = self.get_argument_value(&field.arguments, "newEmail")?;
// Your implementation here:
// 1. Find user by ID
// 2. Validate new email format
// 3. Update email in database
// 4. Return updated user
todo!("Implement updateUserEmail mutation")
}
What happens when a mutation fails? Try this:
mutation {
first: createUser(input: { name: "Alice" }) { id }
second: createUser(input: { name: "" }) { id } # Invalid: empty name
third: createUser(input: { name: "Charlie" }) { id } # Should this execute?
}
Expected behavior: Sequential execution stops at the failed mutation.
Implement a transferMoney
mutation that requires multiple database operations:
mutation {
transferMoney(
fromAccountId: "acc_1",
toAccountId: "acc_2",
amount: 100.00
) {
success
fromAccount { id balance }
toAccount { id balance }
transaction { id timestamp amount }
}
}
Requirements:
You now understand:
Your mutation support is production-ready! ๐