Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ node_modules/
yarn.lock
OperationStoreClient.js
spec/integration/tmp
.vscode/
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ gem 'bootsnap' # required by the Rails apps generated in tests
gem 'ruby-prof', platform: :ruby
gem 'pry'
gem 'pry-stack_explorer', platform: :ruby
gem 'pry-byebug'

# Required for running `jekyll algolia ...` (via `rake site:update_search_index`)
group :jekyll_plugins do
Expand Down
1 change: 1 addition & 0 deletions lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def self.scan_with_ragel(graphql_string)

require "graphql/analysis_error"
require "graphql/coercion_error"
require "graphql/literal_validation_error"
require "graphql/runtime_type_error"
require "graphql/invalid_null_error"
require "graphql/invalid_name_error"
Expand Down
6 changes: 6 additions & 0 deletions lib/graphql/literal_validation_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true
module GraphQL
class LiteralValidationError < GraphQL::Error
attr_accessor :ast_value
end
end
15 changes: 15 additions & 0 deletions lib/graphql/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class Schema
:query_execution_strategy, :mutation_execution_strategy, :subscription_execution_strategy,
:max_depth, :max_complexity, :default_max_page_size,
:orphan_types, :resolve_type, :type_error, :parse_error,
:error_bubbling,
:raise_definition_error,
:object_from_id, :id_from_object,
:default_mask,
Expand Down Expand Up @@ -107,6 +108,9 @@ class Schema
:raise_definition_error,
:introspection_namespace

# [Boolean] True if this object bubbles validation errors up from a field into its parent InputObject, if there is one.
attr_accessor :error_bubbling

# Single, long-lived instance of the provided subscriptions class, if there is one.
# @return [GraphQL::Subscriptions]
attr_accessor :subscriptions
Expand Down Expand Up @@ -168,6 +172,7 @@ def initialize
@context_class = GraphQL::Query::Context
@introspection_namespace = nil
@introspection_system = nil
@error_bubbling = true
end

def initialize_copy(other)
Expand Down Expand Up @@ -659,6 +664,7 @@ class << self
:validate, :multiplex_analyzers, :lazy?, :lazy_method_name, :after_lazy, :sync_lazy,
# Configuration
:max_complexity=, :max_depth=,
:error_bubbling=,
:metadata,
:default_mask,
:default_filter, :redefine,
Expand Down Expand Up @@ -692,6 +698,7 @@ def to_graphql
schema_defn.mutation = mutation
schema_defn.subscription = subscription
schema_defn.max_complexity = max_complexity
schema_defn.error_bubbling = error_bubbling
schema_defn.max_depth = max_depth
schema_defn.default_max_page_size = default_max_page_size
schema_defn.orphan_types = orphan_types
Expand Down Expand Up @@ -823,6 +830,14 @@ def max_complexity(max_complexity = nil)
end
end

def error_bubbling(new_error_bubbling = nil)
if !new_error_bubbling.nil?
@error_bubbling = new_error_bubbling
else
@error_bubbling
end
end

def max_depth(new_max_depth = nil)
if new_max_depth
@max_depth = new_max_depth
Expand Down
1 change: 1 addition & 0 deletions lib/graphql/static_validation/all_rules.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module StaticValidation
GraphQL::StaticValidation::ArgumentsAreDefined,
GraphQL::StaticValidation::ArgumentLiteralsAreCompatible,
GraphQL::StaticValidation::RequiredArgumentsArePresent,
GraphQL::StaticValidation::RequiredInputObjectAttributesArePresent,
GraphQL::StaticValidation::ArgumentNamesAreUnique,
GraphQL::StaticValidation::VariableNamesAreUnique,
GraphQL::StaticValidation::VariablesAreInputTypes,
Expand Down
36 changes: 20 additions & 16 deletions lib/graphql/static_validation/arguments_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@ module GraphQL
module StaticValidation
# Implement validate_node
class ArgumentsValidator
module ArgumentsValidatorHelpers
private

def parent_name(parent, type_defn)
if parent.is_a?(GraphQL::Language::Nodes::Field)
parent.alias || parent.name
elsif parent.is_a?(GraphQL::Language::Nodes::InputObject)
type_defn.name
else
parent.name
end
end

def node_type(parent)
parent.class.name.split("::").last
end
end

include ArgumentsValidatorHelpers
include GraphQL::StaticValidation::Message::MessageHelper

def validate(context)
Expand All @@ -29,22 +48,7 @@ def validate(context)
validate_node(parent, node, parent_defn, context)
}
end

private

def parent_name(parent, type_defn)
if parent.is_a?(GraphQL::Language::Nodes::Field)
parent.alias || parent.name
elsif parent.is_a?(GraphQL::Language::Nodes::InputObject)
type_defn.name
else
parent.name
end
end

def node_type(parent)
parent.class.name.split("::").last
end
end

end
end
65 changes: 54 additions & 11 deletions lib/graphql/static_validation/literal_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,61 @@ def initialize(context:)
end

def validate(ast_value, type)
if ast_value.is_a?(GraphQL::Language::Nodes::NullValue)
!type.kind.non_null?
if type.nil?
# this means we're an undefined argument, see #present_input_field_values_are_valid
return maybe_raise_if_invalid(ast_value) do
false
end
elsif ast_value.is_a?(GraphQL::Language::Nodes::NullValue)
maybe_raise_if_invalid(ast_value) do
!type.kind.non_null?
end
elsif type.kind.non_null?
(!ast_value.nil?) && validate(ast_value, type.of_type)
maybe_raise_if_invalid(ast_value) do
(!ast_value.nil?)
end && validate(ast_value, type.of_type)
elsif type.kind.list?
item_type = type.of_type
ensure_array(ast_value).all? { |val| validate(val, item_type) }
elsif ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
true
elsif type.kind.scalar? && constant_scalar?(ast_value)
type.valid_input?(ast_value, @context)
elsif type.kind.enum? && ast_value.is_a?(GraphQL::Language::Nodes::Enum)
type.valid_input?(ast_value.name, @context)
maybe_raise_if_invalid(ast_value) do
type.valid_input?(ast_value, @context)
end
elsif type.kind.enum?
maybe_raise_if_invalid(ast_value) do
if ast_value.is_a?(GraphQL::Language::Nodes::Enum)
type.valid_input?(ast_value.name, @context)
else
# if our ast_value isn't an Enum it's going to be invalid so return false
false
end
end
elsif type.kind.input_object? && ast_value.is_a?(GraphQL::Language::Nodes::InputObject)
required_input_fields_are_present(type, ast_value) &&
present_input_field_values_are_valid(type, ast_value)
maybe_raise_if_invalid(ast_value) do
required_input_fields_are_present(type, ast_value) && present_input_field_values_are_valid(type, ast_value)
end
else
false
maybe_raise_if_invalid(ast_value) do
false
end
end
end

private

def maybe_raise_if_invalid(ast_value)
ret = yield
if [email protected]_bubbling && !ret
e = LiteralValidationError.new
e.ast_value = ast_value
raise e
else
ret
end
end

# The GraphQL grammar supports variables embedded within scalars but graphql.js
# doesn't support it so we won't either for simplicity
def constant_scalar?(ast_value)
Expand All @@ -47,19 +79,30 @@ def constant_scalar?(ast_value)
end

def required_input_fields_are_present(type, ast_node)
# TODO - would be nice to use these to create an error message so the caller knows
# that required fields are missing
required_field_names = @warden.arguments(type)
.select { |f| f.type.kind.non_null? }
.map(&:name)
present_field_names = ast_node.arguments.map(&:name)
missing_required_field_names = required_field_names - present_field_names
missing_required_field_names.none?
if @context.schema.error_bubbling
missing_required_field_names.none?
else
missing_required_field_names.all? do |name|
validate(GraphQL::Language::Nodes::NullValue.new(name: name), @warden.arguments(type).find { |f| f.name == name }.type )
end
end
end

def present_input_field_values_are_valid(type, ast_node)
field_map = @warden.arguments(type).reduce({}) { |m, f| m[f.name] = f; m}
ast_node.arguments.all? do |value|
field = field_map[value.name]
field && validate(value.value, field.type)
# we want to call validate on an argument even if it's an invalid one
# so that our raise exception is on it instead of the entire InputObject
type = field && field.type
validate(value.value, type)
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,27 @@ def validate_node(parent, node, defn, context)
valid = context.valid_literal?(node.value, arg_defn.type)
rescue GraphQL::CoercionError => err
error_message = err.message
context.schema.error_bubbling
if !context.schema.error_bubbling && !arg_defn.type.unwrap.kind.scalar?
# if error bubbling is disabled and the arg that caused this error isn't a scalar then
# short-circuit here so we avoid bubbling this up to whatever input_object / array contains us
return false
end
rescue GraphQL::LiteralValidationError => err
# check to see if the ast node that caused the error to be raised is
# the same as the node we were checking here.
matched = if arg_defn.type.kind.list?
# for a list we claim an error if the node is contained in our list
node.value.include?(err.ast_value)
elsif arg_defn.type.kind.input_object? && node.value.is_a?(GraphQL::Language::Nodes::InputObject)
# for an input object we check the arguments
node.value.arguments.include?(err.ast_value)
else
# otherwise we just check equality
node.value == (err.ast_value)
end
return false unless matched
end

return if valid

error_message ||= begin
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true
module GraphQL
module StaticValidation
class RequiredInputObjectAttributesArePresent
include GraphQL::StaticValidation::Message::MessageHelper
include GraphQL::StaticValidation::ArgumentsValidator::ArgumentsValidatorHelpers

def validate(context)
visitor = context.visitor
visitor[GraphQL::Language::Nodes::InputObject] << ->(node, parent) {
next unless parent.is_a? GraphQL::Language::Nodes::Argument
validate_input_object(node, context, parent)
}
end

private

def get_parent_type(context, parent)
defn = context.field_definition
parent_type = context.warden.arguments(defn)
.find{|f| f.name == parent_name(parent, defn) }
parent_type ? parent_type.type.unwrap : nil
end

def validate_input_object(ast_node, context, parent)
parent_type = get_parent_type(context, parent)
return unless parent_type && parent_type.kind.input_object?

required_fields = parent_type.arguments
.select{|k,v| v.type.kind.non_null?}
.keys

present_fields = ast_node.arguments.map(&:name)
missing_fields = required_fields - present_fields

missing_fields.each do |missing_field|
path = [ *context.path, missing_field]
missing_field_type = parent_type.arguments[missing_field].type
context.errors << message("Argument '#{missing_field}' on InputObject '#{parent_type}' is required. Expected type #{missing_field_type}", ast_node, path: path, context: context)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ def validate_default_value(node, context)
valid = context.valid_literal?(value, type)
rescue GraphQL::CoercionError => err
error_message = err.message
rescue GraphQL::LiteralValidationError
# noop, we just want to stop any LiteralValidationError from propagating
end

if !valid
Expand Down
45 changes: 34 additions & 11 deletions spec/graphql/schema/warden_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true
require "spec_helper"
include ErrorBubblingHelpers

module MaskHelpers
PhonemeType = GraphQL::ObjectType.define do
Expand Down Expand Up @@ -630,17 +631,39 @@ def error_messages(query_result)
assert_equal expected_errors, error_messages(res)
end

it "isn't a valid literal input" do
query_string = %|
{
languages(within: {latitude: 1.0, longitude: 2.2, miles: 3.3}) { name }
}|
res = MaskHelpers.query_with_mask(query_string, mask)
expected_errors = [
"Argument 'within' on Field 'languages' has an invalid value. Expected type 'WithinInput'.",
"InputObject 'WithinInput' doesn't accept argument 'miles'"
]
assert_equal expected_errors, error_messages(res)
describe "with error bubbling disabled" do
it "isn't a valid literal input" do
without_error_bubbling(MaskHelpers::Schema) do
query_string = %|
{
languages(within: {latitude: 1.0, longitude: 2.2, miles: 3.3}) { name }
}|
res = MaskHelpers.query_with_mask(query_string, mask)
expected_errors =
[
"InputObject 'WithinInput' doesn't accept argument 'miles'"
]
assert_equal expected_errors, error_messages(res)
end
end
end

describe "with error bubbling enabled" do
it "isn't a valid literal input" do
with_error_bubbling(MaskHelpers::Schema) do
query_string = %|
{
languages(within: {latitude: 1.0, longitude: 2.2, miles: 3.3}) { name }
}|
res = MaskHelpers.query_with_mask(query_string, mask)
expected_errors =
[
"Argument 'within' on Field 'languages' has an invalid value. Expected type 'WithinInput'.",
"InputObject 'WithinInput' doesn't accept argument 'miles'"
]
assert_equal expected_errors, error_messages(res)
end
end
end

it "isn't a valid variable input" do
Expand Down
Loading