# frozen_string_literal: true module GraphQL class Query # Read-only access to query variables, applying default values if needed. class Variables extend Forwardable # @return [Array] Any errors encountered when parsing the provided variables and literal values attr_reader :errors attr_reader :context def initialize(ctx, ast_variables, provided_variables) schema = ctx.schema @context = ctx @provided_variables = GraphQL::Argument.deep_stringify(provided_variables) @errors = [] @storage = ast_variables.each_with_object({}) do |ast_variable, memo| if schema.validate_max_errors && schema.validate_max_errors <= @errors.count add_max_errors_reached_message break end # Find the right value for this variable: # - First, use the value provided at runtime # - Then, fall back to the default value from the query string # If it's still nil, raise an error if it's required. variable_type = schema.type_from_ast(ast_variable.type, context: ctx) if variable_type.nil? # Pass -- it will get handled by a validator else variable_name = ast_variable.name default_value = ast_variable.default_value provided_value = @provided_variables[variable_name] value_was_provided = @provided_variables.key?(variable_name) max_errors = schema.validate_max_errors - @errors.count if schema.validate_max_errors begin validation_result = variable_type.validate_input(provided_value, ctx, max_errors: max_errors) if validation_result.valid? if value_was_provided # Add the variable if a value was provided memo[variable_name] = if ctx.interpreter? provided_value elsif provided_value.nil? nil else schema.error_handler.with_error_handling(context) do variable_type.coerce_input(provided_value, ctx) end end elsif default_value != nil memo[variable_name] = if ctx.interpreter? if default_value.is_a?(Language::Nodes::NullValue) nil else default_value end else # Add the variable if it wasn't provided but it has a default value (including `null`) GraphQL::Query::LiteralInput.coerce(variable_type, default_value, self) end end end rescue GraphQL::ExecutionError => ex # TODO: This should really include the path to the problematic node in the variable value # like InputValidationResults generated by validate_non_null_input but unfortunately we don't # have this information available in the coerce_input call chain. Note this path is the path # that appears under errors.extensions.problems.path and NOT the result path under errors.path. validation_result = GraphQL::Query::InputValidationResult.from_problem(ex.message) end if !validation_result.valid? @errors << GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, provided_value, validation_result) end end end end def_delegators :@storage, :length, :key?, :[], :fetch, :to_h private def deep_stringify(val) case val when Array val.map { |v| deep_stringify(v) } when Hash new_val = {} val.each do |k, v| new_val[k.to_s] = deep_stringify(v) end new_val else val end end def add_max_errors_reached_message message = "Too many errors processing variables, max validation error limit reached. Execution aborted" validation_result = GraphQL::Query::InputValidationResult.from_problem(message) errors << GraphQL::Query::VariableValidationError.new(nil, nil, nil, validation_result, msg: message) end end end end