fraiseql-core 2.12.0

Core execution engine for FraiseQL v2 - Compiled GraphQL over SQL
Documentation
//! Input processor for GraphQL variables with ID policy validation
//!
//! This module provides utilities to validate GraphQL input variables,
//! particularly ID fields, according to the configured ID policy.
//!
//! **SECURITY CRITICAL**: Input validation is a critical security layer that
//! prevents invalid data from propagating through the GraphQL pipeline.

use std::collections::HashSet;

use serde_json::{Map, Value};

use super::id_policy::{IDPolicy, validate_id};

/// Configuration for input processing
#[derive(Debug, Clone)]
pub struct InputProcessingConfig {
    /// ID policy to enforce for ID fields
    pub id_policy: IDPolicy,

    /// Enable ID validation on all inputs (recommended)
    pub validate_ids: bool,

    /// List of field names known to be ID types
    /// (in a real implementation, this would come from the schema)
    pub id_field_names: HashSet<String>,
}

impl Default for InputProcessingConfig {
    fn default() -> Self {
        Self {
            id_policy:      IDPolicy::default(),
            validate_ids:   true,
            id_field_names: Self::default_id_field_names(),
        }
    }
}

impl InputProcessingConfig {
    /// Get default set of common ID field names
    fn default_id_field_names() -> HashSet<String> {
        [
            "id",
            "userId",
            "user_id",
            "postId",
            "post_id",
            "commentId",
            "comment_id",
            "authorId",
            "author_id",
            "ownerId",
            "owner_id",
            "creatorId",
            "creator_id",
            "tenantId",
            "tenant_id",
        ]
        .iter()
        .map(|s| (*s).to_string())
        .collect()
    }

    /// Add a custom ID field name to validation
    pub fn add_id_field(&mut self, field_name: String) {
        self.id_field_names.insert(field_name);
    }

    /// Create a configuration for strict UUID validation
    #[must_use]
    pub fn strict_uuid() -> Self {
        Self {
            id_policy:      IDPolicy::UUID,
            validate_ids:   true,
            id_field_names: Self::default_id_field_names(),
        }
    }

    /// Create a configuration for opaque IDs (GraphQL spec compliant)
    #[must_use]
    pub fn opaque() -> Self {
        Self {
            id_policy:      IDPolicy::OPAQUE,
            validate_ids:   false, // No validation needed for opaque
            id_field_names: Self::default_id_field_names(),
        }
    }
}

/// Process and validate GraphQL input variables
///
/// **SECURITY CRITICAL**: This validates all ID fields in input variables
/// according to the configured ID policy.
///
/// # Arguments
///
/// * `variables` - GraphQL operation variables (JSON object)
/// * `config` - Input processing configuration
///
/// # Returns
///
/// `Ok(processed_variables)` with validated data, or
/// `Err(ProcessingError)` if validation fails
///
/// # Errors
///
/// Returns `ProcessingError` if any ID field fails validation according to the configured policy.
///
/// # Examples
///
/// ```
/// use fraiseql_core::validation::{InputProcessingConfig, process_variables};
/// use serde_json::json;
///
/// let config = InputProcessingConfig::strict_uuid();
/// let variables = json!({"userId": "550e8400-e29b-41d4-a716-446655440000"});
///
/// match process_variables(&variables, &config) {
///     Ok(processed) => { /* Use processed variables */ },
///     Err(_e) => { /* Handle validation error */ },
/// }
/// ```
pub fn process_variables(
    variables: &Value,
    config: &InputProcessingConfig,
) -> Result<Value, ProcessingError> {
    if !config.validate_ids {
        return Ok(variables.clone());
    }

    match variables {
        Value::Object(obj) => {
            let mut result = Map::new();

            for (key, value) in obj {
                let processed_value = process_value(value, config, key)?;
                result.insert(key.clone(), processed_value);
            }

            Ok(Value::Object(result))
        },
        Value::Null => Ok(Value::Null),
        other => Ok(other.clone()),
    }
}

/// Process a single JSON value, validating ID fields
fn process_value(
    value: &Value,
    config: &InputProcessingConfig,
    field_name: &str,
) -> Result<Value, ProcessingError> {
    match value {
        // Validate ID string fields
        // Extract base field name (before array indices like [0])
        Value::String(s)
            if {
                let base_field = field_name.split('[').next().unwrap_or(field_name);
                config.id_field_names.contains(base_field)
            } =>
        {
            validate_id(s, config.id_policy).map_err(|e| ProcessingError {
                field_path: field_name.to_string(),
                reason:     format!("Invalid ID value: {e}"),
            })?;
            Ok(Value::String(s.clone()))
        },

        // Recursively process nested objects
        Value::Object(obj) => {
            let mut result = Map::new();

            for (key, nested_value) in obj {
                let processed = process_value(nested_value, config, key)?;
                result.insert(key.clone(), processed);
            }

            Ok(Value::Object(result))
        },

        // Process array items
        Value::Array(arr) => {
            let processed_items: Result<Vec<_>, _> = arr
                .iter()
                .enumerate()
                .map(|(idx, item)| {
                    let array_field = format!("{field_name}[{idx}]");
                    process_value(item, config, &array_field)
                })
                .collect();

            Ok(Value::Array(processed_items?))
        },

        // Pass through other values unchanged
        other => Ok(other.clone()),
    }
}

/// Error type for input processing failures
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProcessingError {
    /// The field path where the error occurred
    pub field_path: String,
    /// The reason for the error
    pub reason:     String,
}

impl std::fmt::Display for ProcessingError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error in field '{}': {}", self.field_path, self.reason)
    }
}

impl std::error::Error for ProcessingError {}