Skip to content

Query-level schema restrictions #300

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Oct 27, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
refactor(BaseType#validate_input) thread the Warden through all input…
… validation
  • Loading branch information
rmosolgo committed Oct 26, 2016
commit 065e9bde5f12d336eb368fc1ed834fdf94fd11a7
13 changes: 8 additions & 5 deletions lib/graphql/base_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,16 @@ def to_s

alias :inspect :to_s

def valid_input?(value)
validate_input(value).valid?
def valid_input?(value, warden)
validate_input(value, warden).valid?
end

def validate_input(value)
return GraphQL::Query::InputValidationResult.new if value.nil?
validate_non_null_input(value)
def validate_input(value, warden)
if value.nil?
GraphQL::Query::InputValidationResult.new
else
validate_non_null_input(value, warden)
end
end

def coerce_input(value)
Expand Down
5 changes: 0 additions & 5 deletions lib/graphql/define/defined_object_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ def types
GraphQL::Define::TypeDefiner.instance
end

# TODO: do I actually want to add this?
def metadata(key, value)
@target.metadata[key] = value
end

def method_missing(name, *args, &block)
definition = @dictionary[name]
if definition
Expand Down
8 changes: 5 additions & 3 deletions lib/graphql/enum_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,14 @@ def kind
GraphQL::TypeKinds::ENUM
end

def validate_non_null_input(value_name)
def validate_non_null_input(value_name, warden)
ensure_defined
result = GraphQL::Query::InputValidationResult.new
allowed_values = warden.enum_values(self)
matching_value = allowed_values.find { |v| v.name == value_name }

if !@values_by_name.key?(value_name)
result.add_problem("Expected #{JSON.generate(value_name, quirks_mode: true)} to be one of: #{@values_by_name.keys.join(', ')}")
if matching_value.nil?
result.add_problem("Expected #{JSON.generate(value_name, quirks_mode: true)} to be one of: #{allowed_values.join(', ')}")
end

result
Expand Down
10 changes: 6 additions & 4 deletions lib/graphql/input_object_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,21 @@ def kind
GraphQL::TypeKinds::INPUT_OBJECT
end

def validate_non_null_input(input)
def validate_non_null_input(input, warden)
result = GraphQL::Query::InputValidationResult.new

visible_arguments_map = warden.input_fields(self).reduce({}) { |m, f| m[f.name] = f; m}

# Items in the input that are unexpected
input.each do |name, value|
if arguments[name].nil?
if visible_arguments_map[name].nil?
result.add_problem("Field is not defined on #{self.name}", [name])
end
end

# Items in the input that are expected, but have invalid values
invalid_fields = arguments.map do |name, field|
field_result = field.type.validate_input(input[name])
invalid_fields = visible_arguments_map.map do |name, field|
field_result = field.type.validate_input(input[name], warden)
if !field_result.valid?
result.merge_result!(name, field_result)
end
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/introspection/input_fields_field.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
GraphQL::Introspection::InputFieldsField = GraphQL::Field.define do
name "inputFields"
type types[!GraphQL::Introspection::InputValueType]
resolve ->(target, a, c) {
resolve ->(target, a, ctx) {
if target.kind.input_object?
target.input_fields.values
ctx.warden.input_fields(target)
else
nil
end
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/list_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ def to_s
"[#{of_type.to_s}]"
end

def validate_non_null_input(value)
def validate_non_null_input(value, warden)
result = GraphQL::Query::InputValidationResult.new

ensure_array(value).each_with_index do |item, index|
item_result = of_type.validate_input(item)
item_result = of_type.validate_input(item, warden)
if !item_result.valid?
result.merge_result!(index, item_result)
end
Expand Down
8 changes: 4 additions & 4 deletions lib/graphql/non_null_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ def initialize(of_type:)
@of_type = of_type
end

def valid_input?(value)
validate_input(value).valid?
def valid_input?(value, warden)
validate_input(value, warden).valid?
end

def validate_input(value)
def validate_input(value, warden)
if value.nil?
result = GraphQL::Query::InputValidationResult.new
result.add_problem("Expected value to not be null")
result
else
of_type.validate_input(value)
of_type.validate_input(value, warden)
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/graphql/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def variables
@variables ||= begin
vars = GraphQL::Query::Variables.new(
@schema,
@warden,
@ast_variables,
@provided_variables,
)
Expand Down
5 changes: 3 additions & 2 deletions lib/graphql/query/variables.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ class Variables
# @return [Array<GraphQL::Query::VariableValidationError>] Any errors encountered when parsing the provided variables and literal values
attr_reader :errors

def initialize(schema, ast_variables, provided_variables)
def initialize(schema, warden, ast_variables, provided_variables)
@schema = schema
@warden = warden
@provided_variables = provided_variables
@errors = []
@storage = ast_variables.each_with_object({}) do |ast_variable, memo|
Expand All @@ -31,7 +32,7 @@ def get_graphql_value(ast_variable)
default_value = ast_variable.default_value
provided_value = @provided_variables[variable_name]

validation_result = variable_type.validate_input(provided_value)
validation_result = variable_type.validate_input(provided_value, @warden)
if !validation_result.valid?
@errors << GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, provided_value, validation_result)
elsif provided_value.nil?
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/scalar_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def coerce=(proc)
self.coerce_result = proc
end

def validate_non_null_input(value)
def validate_non_null_input(value, warden)
result = Query::InputValidationResult.new
if coerce_non_null_input(value).nil?
result.add_problem("Could not coerce value #{JSON.generate(value, quirks_mode: true)} to #{name}")
Expand Down
9 changes: 6 additions & 3 deletions lib/graphql/schema/warden.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,7 @@ def fields(type_defn)
# @param argument_owner [GraphQL::Field, GraphQL::InputObjectType]
# @return [Array<GraphQL::Argument>] Visible arguments on `argument_owner`
def arguments(argument_owner)
argument_owner.arguments.each_value.select do |arg_defn|
visible?(arg_defn) && visible?(arg_defn.type.unwrap)
end
argument_owner.arguments.each_value.select { |a| visible_field?(a) }
end

# @return [Array<GraphQL::EnumType::EnumValue>] Visible members of `enum_defn`
Expand All @@ -97,6 +95,11 @@ def interfaces(obj_type)
obj_type.interfaces.select { |t| visible?(t) }
end

# @return [Array<GraphQL::Field>] Visible input fields on `input_obj_type`
def input_fields(input_obj_type)
input_obj_type.arguments.each_value.select { |f| visible_field?(f) }
end

private

def visible_field?(field_defn)
Expand Down
14 changes: 6 additions & 8 deletions lib/graphql/static_validation/literal_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ def validate(ast_value, type)
item_type = type.of_type
ensure_array(ast_value).all? { |val| validate(val, item_type) }
elsif type.kind.scalar? && !ast_value.is_a?(GraphQL::Language::Nodes::AbstractNode) && !ast_value.is_a?(Array)
type.valid_input?(ast_value)
type.valid_input?(ast_value, @warden)
elsif type.kind.enum? && ast_value.is_a?(GraphQL::Language::Nodes::Enum)
# TODO: this shortcuts the `valid_input?` API, should I improve that API instead of bypassing it?
@warden.enum_values(type).find { |enum_value_defn| enum_value_defn.name == ast_value.name }
type.valid_input?(ast_value.name, @warden)
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)
Expand All @@ -32,8 +31,7 @@ def validate(ast_value, type)


def required_input_fields_are_present(type, ast_node)
required_field_names = type.input_fields
.values
required_field_names = @warden.input_fields(type)
.select { |f| f.type.kind.non_null? }
.map(&:name)
present_field_names = ast_node.arguments.map(&:name)
Expand All @@ -42,10 +40,10 @@ def required_input_fields_are_present(type, ast_node)
end

def present_input_field_values_are_valid(type, ast_node)
fields = type.input_fields
field_map = @warden.input_fields(type).reduce({}) { |m, f| m[f.name] = f; m}
ast_node.arguments.all? do |value|
field = fields[value.name]
field ? validate(value.value, field.type) : true
field = field_map[value.name]
field && validate(value.value, field.type)
end
end

Expand Down
10 changes: 2 additions & 8 deletions spec/graphql/enum_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,12 @@
end

describe "resolving with a warden" do
module ExampleWarden
def self.enum_values(enum_type)
[]
end
end

it "gets values from the warden" do
# OK
assert_equal("YAK", enum.coerce_result("YAK"))
# NOT OK
assert_raises(GraphQL::EnumType::UnresolvedValueError) {
enum.coerce_result("YAK", ExampleWarden)
enum.coerce_result("YAK", NothingWarden)
}
end
end
Expand All @@ -45,7 +39,7 @@ def self.enum_values(enum_type)
end

describe "validate_input with bad input" do
let(:result) { DairyAnimalEnum.validate_input("bad enum") }
let(:result) { DairyAnimalEnum.validate_input("bad enum", PermissiveWarden) }

it "returns an invalid result" do
assert(!result.valid?)
Expand Down
18 changes: 9 additions & 9 deletions spec/graphql/input_object_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
describe "input validation" do
it "Accepts anything that yields key-value pairs to #all?" do
values_obj = MinimumInputObject.new({"source" => "COW", "fatContent" => 0.4})
assert DairyProductInputType.valid_input?(values_obj)
assert DairyProductInputType.valid_input?(values_obj, PermissiveWarden)
end

describe "validate_input with non-enumerable input" do
it "returns a valid result for MinimumInputObject" do
result = DairyProductInputType.validate_input(MinimumInputObject.new({"source" => "COW", "fatContent" => 0.4}))
result = DairyProductInputType.validate_input(MinimumInputObject.new({"source" => "COW", "fatContent" => 0.4}), PermissiveWarden)
assert(result.valid?)
end

it "returns an invalid result for MinimumInvalidInputObject" do
invalid_input = MinimumInputObject.new({"source" => "KOALA", "fatContent" => 0.4})
result = DairyProductInputType.validate_input(invalid_input)
result = DairyProductInputType.validate_input(invalid_input, PermissiveWarden)
assert(!result.valid?)
end
end
Expand All @@ -37,15 +37,15 @@
"fatContent" => 0.4
}
end
let(:result) { DairyProductInputType.validate_input(input) }
let(:result) { DairyProductInputType.validate_input(input, PermissiveWarden) }

it "returns a valid result" do
assert(result.valid?)
end
end

describe "with bad enum and float" do
let(:result) { DairyProductInputType.validate_input("source" => "KOALA", "fatContent" => "bad_num") }
let(:result) { DairyProductInputType.validate_input({"source" => "KOALA", "fatContent" => "bad_num"}, PermissiveWarden) }

it "returns an invalid result" do
assert(!result.valid?)
Expand All @@ -58,7 +58,7 @@
end

it "has correct problem explanation" do
expected = DairyAnimalEnum.validate_input("KOALA").problems[0]["explanation"]
expected = DairyAnimalEnum.validate_input("KOALA", PermissiveWarden).problems[0]["explanation"]

source_problem = result.problems.detect { |p| p["path"] == ["source"] }
actual = source_problem["explanation"]
Expand All @@ -68,7 +68,7 @@
end

describe "with extra argument" do
let(:result) { DairyProductInputType.validate_input("source" => "COW", "fatContent" => 0.4, "isDelicious" => false) }
let(:result) { DairyProductInputType.validate_input({"source" => "COW", "fatContent" => 0.4, "isDelicious" => false}, PermissiveWarden) }

it "returns an invalid result" do
assert(!result.valid?)
Expand All @@ -90,7 +90,7 @@
list_type.validate_input([
{ "source" => "COW", "fatContent" => 0.4 },
{ "source" => "KOALA", "fatContent" => 0.4 }
])
], PermissiveWarden)
end

it "returns an invalid result" do
Expand All @@ -107,7 +107,7 @@
end

it "has problem with correct explanation" do
expected = DairyAnimalEnum.validate_input("KOALA").problems[0]["explanation"]
expected = DairyAnimalEnum.validate_input("KOALA", PermissiveWarden).problems[0]["explanation"]
actual = result.problems[0]["explanation"]
assert_equal(expected, actual)
end
Expand Down
4 changes: 2 additions & 2 deletions spec/graphql/list_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

describe "validate_input with bad input" do
let(:bad_num) { "bad_num" }
let(:result) { float_list.validate_input([bad_num, 2.0, 3.0]) }
let(:result) { float_list.validate_input([bad_num, 2.0, 3.0], PermissiveWarden) }

it "returns an invalid result" do
assert(!result.valid?)
Expand All @@ -24,7 +24,7 @@
end

it "has the correct explanation" do
expected = GraphQL::FLOAT_TYPE.validate_input(bad_num).problems[0]["explanation"]
expected = GraphQL::FLOAT_TYPE.validate_input(bad_num, PermissiveWarden).problems[0]["explanation"]
actual = result.problems[0]["explanation"]
assert_equal(actual, expected)
end
Expand Down
1 change: 1 addition & 0 deletions spec/graphql/query/variables_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
let(:ast_variables) { GraphQL.parse(query_string).definitions.first.variables }
let(:variables) { GraphQL::Query::Variables.new(
DummySchema,
GraphQL::Schema::Warden.new(DummySchema, GraphQL::Query::NullExcept),
ast_variables,
provided_variables)
}
Expand Down
6 changes: 3 additions & 3 deletions spec/graphql/scalar_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
end

describe "custom scalar errors" do
let(:result) { custom_scalar.validate_input("xyz") }
let(:result) { custom_scalar.validate_input("xyz", PermissiveWarden) }

it "returns an invalid result" do
assert !result.valid?
Expand All @@ -32,15 +32,15 @@
end

describe "validate_input with good input" do
let(:result) { GraphQL::INT_TYPE.validate_input(150) }
let(:result) { GraphQL::INT_TYPE.validate_input(150, PermissiveWarden) }

it "returns a valid result" do
assert(result.valid?)
end
end

describe "validate_input with bad input" do
let(:result) { GraphQL::INT_TYPE.validate_input("bad num") }
let(:result) { GraphQL::INT_TYPE.validate_input("bad num", PermissiveWarden) }

it "returns an invalid result for bad input" do
assert(!result.valid?)
Expand Down
Loading