diff --git a/README.md b/README.md index f35c0ec7b43..6fd9b578295 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ dotnet new graphql-server ## Features -We currently support the following parts of the current [draft spec](http://facebook.github.io/graphql/draft/) of GraphQL. +We currently support the following parts of the current [June 2018 specification](http://facebook.github.io/graphql/June2018/) of GraphQL. ### Types @@ -210,33 +210,35 @@ We currently support the following parts of the current [draft spec](http://face - [x] Skip - [x] Continue -- [ ] Deprecated +- [ ] _Deprecated_ (in development - 0.5.0) ### Validation -- [ ] [Validation](https://github.com/ChilliCream/hotchocolate/projects/3) +- [x] [_Validation_](https://github.com/ChilliCream/hotchocolate/projects/3) ### Execution - [x] Query - [x] Mutation -- [ ] Subscription +- [ ] _Subscription_ (in development - 0.5.0) ### Introspection - Fields - - [x] __typename - - [x] __type - - [x] __schema -- __Schema + - [x] \_\_typename + - [x] \_\_type + - [x] \_\_schema + +- \_\_Schema + - [x] types - [x] queryType - [x] mutationType - [x] subscriptionType - [x] directives -- __Type +- \_\_Type - [x] kind - [x] name - [x] fields @@ -254,6 +256,7 @@ Moreover, we are working on the following parts that are not defined in the spec - [x] Date - [ ] Time - [ ] URL +- [ ] UUID - [x] Decimal - [x] Short (Int16) - [x] Long (Int64) @@ -264,8 +267,8 @@ Moreover, we are working on the following parts that are not defined in the spec - [ ] Export - [ ] Defer - [ ] Stream -- [ ] Custom Schema Directives -- [ ] Custom Execution Directives +- [ ] _Custom Schema Directives_ (in development - 0.5.0) +- [ ] _Custom Execution Directives_ (in development - 0.5.0) ### Execution Engine @@ -280,6 +283,7 @@ Moreover, we are working on the following parts that are not defined in the spec ## Supported Frameworks - [ ] ASP.NET Classic + - [ ] Get - [ ] Post diff --git a/src/Core.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs b/src/Core.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs index 09a1abdf680..07b4a2e0af3 100644 --- a/src/Core.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs +++ b/src/Core.Tests/Integration/StarWarsCodeFirst/StarWarsCodeFirstTests.cs @@ -460,6 +460,28 @@ query op($ep: [Episode!]!) Assert.Equal(Snapshot.Current(), Snapshot.New(result)); } + [Fact] + public void ConditionalInlineFragment() + { + // arrange + Schema schema = CreateSchema(); + string query = @" + { + heroes(episodes: [EMPIRE]) { + name + ... @include(if: true) { + height + } + } + }"; + + // act + IExecutionResult result = schema.Execute(query); + + // assert + Assert.Equal(Snapshot.Current(), Snapshot.New(result)); + } + private static Schema CreateSchema() { CharacterRepository repository = new CharacterRepository(); diff --git a/src/Core.Tests/Types/FloatTypeTests.cs b/src/Core.Tests/Types/FloatTypeTests.cs index 5fda20bc427..7a77ad29347 100644 --- a/src/Core.Tests/Types/FloatTypeTests.cs +++ b/src/Core.Tests/Types/FloatTypeTests.cs @@ -11,7 +11,7 @@ public class FloatTypeTests new FloatValueNode("1.000000E+000"); protected override IValueNode GetWrongValueNode => - new IntValueNode("1"); + new StringValueNode("1"); protected override double GetValue => 1.0d; @@ -54,5 +54,34 @@ public void ParseValue_Float_Min() // assert Assert.Equal("-3.402823E+038", literal.Value); } + + [Fact] + public void IsInstanceOfType_IntValueNode() + { + // arrange + FloatType type = new FloatType(); + IntValueNode input = new IntValueNode("123"); + + // act + bool result = type.IsInstanceOfType(input); + + // assert + Assert.True(result); + } + + [Fact] + public void ParseLiteral_IntValueNode() + { + // arrange + FloatType type = new FloatType(); + IntValueNode input = new IntValueNode("123"); + + // act + object result = type.ParseLiteral(input); + + // assert + Assert.IsType(result); + Assert.Equal(123d, result); + } } } diff --git a/src/Core.Tests/Validation/DirectivesAreDefinedRuleTests.cs b/src/Core.Tests/Validation/DirectivesAreDefinedRuleTests.cs new file mode 100644 index 00000000000..7d37d90c890 --- /dev/null +++ b/src/Core.Tests/Validation/DirectivesAreDefinedRuleTests.cs @@ -0,0 +1,59 @@ +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class DirectivesAreDefinedRuleTests + : ValidationTestBase + { + public DirectivesAreDefinedRuleTests() + : base(new DirectivesAreDefinedRule()) + { + } + + [Fact] + public void SupportedDirective() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + name @skip(if: true) + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void UnsupportedDirective() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + name @foo(bar: true) + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "The specified directive `foo` " + + "is not supported by the current schema.", + t.Message)); + } + } +} diff --git a/src/Core.Tests/Validation/FragmentSpreadIsPossibleRuleTests.cs b/src/Core.Tests/Validation/FragmentSpreadIsPossibleRuleTests.cs new file mode 100644 index 00000000000..5c6f16f6d6f --- /dev/null +++ b/src/Core.Tests/Validation/FragmentSpreadIsPossibleRuleTests.cs @@ -0,0 +1,114 @@ +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class FragmentSpreadIsPossibleRuleTests + : ValidationTestBase + { + public FragmentSpreadIsPossibleRuleTests() + : base(new FragmentSpreadIsPossibleRule()) + { + } + + [Fact] + public void FragmentDoesNotMatchType() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...fragmentDoesNotMatchType + } + } + + fragment fragmentDoesNotMatchType on Human { + name + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The parent type does not match the type condition on " + + "the fragment `fragmentDoesNotMatchType`.")); + } + + [Fact] + public void InterfaceTypeDoesMatch() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...interfaceTypeDoesMatch + } + } + + fragment interfaceTypeDoesMatch on Pet { + name + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void UnionTypeDoesMatch() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...unionTypeDoesMatch + } + } + + fragment unionTypeDoesMatch on CatOrDog { + name + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void ObjectTypeDoesMatch() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...objectTypeDoesMatch + } + } + + fragment objectTypeDoesMatch on Dog { + name + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + } +} diff --git a/src/Core.Tests/Validation/FragmentSpreadTargetDefinedRuleTests.cs b/src/Core.Tests/Validation/FragmentSpreadTargetDefinedRuleTests.cs new file mode 100644 index 00000000000..a1994b7ddcd --- /dev/null +++ b/src/Core.Tests/Validation/FragmentSpreadTargetDefinedRuleTests.cs @@ -0,0 +1,62 @@ +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class FragmentSpreadTargetDefinedRuleTests + : ValidationTestBase + { + public FragmentSpreadTargetDefinedRuleTests() + : base(new FragmentSpreadTargetDefinedRule()) + { + } + + [Fact] + public void UndefinedFragment() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...undefinedFragment + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The specified fragment `undefinedFragment` does not exist.")); + } + + [Fact] + public void DefinedFragment() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...definedFragment + } + } + + fragment definedFragment on Dog + { + barkVolume + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + } +} diff --git a/src/Core.Tests/Validation/FragmentSpreadTypeExistenceRuleTests.cs b/src/Core.Tests/Validation/FragmentSpreadTypeExistenceRuleTests.cs new file mode 100644 index 00000000000..af25ae234dd --- /dev/null +++ b/src/Core.Tests/Validation/FragmentSpreadTypeExistenceRuleTests.cs @@ -0,0 +1,148 @@ +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class FragmentSpreadTypeExistenceRuleTests + : ValidationTestBase + { + public FragmentSpreadTypeExistenceRuleTests() + : base(new FragmentSpreadTypeExistenceRule()) + { + } + + [Fact] + public void CorrectTypeOnFragment() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...correctType + } + } + + fragment correctType on Dog { + name + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void CorrectTypeOnInlineFragment() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...inlineFragment + } + } + + fragment inlineFragment on Dog { + ... on Dog { + name + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void CorrectTypeOnInlineFragment2() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...inlineFragment2 + } + } + + fragment inlineFragment2 on Dog { + ... @include(if: true) { + name + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void NotOnExistingTypeOnFragment() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...notOnExistingType + } + } + + fragment notOnExistingType on NotInSchema { + name + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The type of fragment `notOnExistingType` " + + "does not exist in the current schema.")); + } + + [Fact] + public void NotExistingTypeOnInlineFragment() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...inlineNotExistingType + } + } + + fragment inlineNotExistingType on Dog { + ... on NotInSchema { + name + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The specified inline fragment " + + "does not exist in the current schema.")); + } + } +} diff --git a/src/Core.Tests/Validation/FragmentSpreadsMustNotFormCyclesRuleTests.cs b/src/Core.Tests/Validation/FragmentSpreadsMustNotFormCyclesRuleTests.cs new file mode 100644 index 00000000000..fb357915d79 --- /dev/null +++ b/src/Core.Tests/Validation/FragmentSpreadsMustNotFormCyclesRuleTests.cs @@ -0,0 +1,166 @@ +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class FragmentSpreadsMustNotFormCyclesRuleTests + : ValidationTestBase + { + public FragmentSpreadsMustNotFormCyclesRuleTests() + : base(new FragmentSpreadsMustNotFormCyclesRule()) + { + } + + [Fact] + public void FragmentCycle1() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...nameFragment + } + } + + fragment nameFragment on Dog { + name + ...barkVolumeFragment + } + + fragment barkVolumeFragment on Dog { + barkVolume + ...nameFragment + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The graph of fragment spreads must not form any " + + "cycles including spreading itself. Otherwise an " + + "operation could infinitely spread or infinitely " + + "execute on cycles in the underlying data.")); + } + + [Fact] + public void FragmentCycle2() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...nameFragment + } + } + + fragment nameFragment on Dog { + name + ...barkVolumeFragment + } + + fragment barkVolumeFragment on Dog { + barkVolume + ...barkVolumeFragment1 + } + + fragment barkVolumeFragment1 on Dog { + barkVolume + ...barkVolumeFragment2 + } + + fragment barkVolumeFragment2 on Dog { + barkVolume + ...nameFragment + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The graph of fragment spreads must not form any " + + "cycles including spreading itself. Otherwise an " + + "operation could infinitely spread or infinitely " + + "execute on cycles in the underlying data.")); + } + + [Fact] + public void InfiniteRecursion() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...dogFragment + } + } + + fragment dogFragment on Dog { + name + owner { + ...ownerFragment + } + } + + fragment ownerFragment on Human { + name + pets { + ...dogFragment + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The graph of fragment spreads must not form any " + + "cycles including spreading itself. Otherwise an " + + "operation could infinitely spread or infinitely " + + "execute on cycles in the underlying data.")); + } + + [Fact] + public void QueryWithSideBySideFragSpreads() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...dogFragment + ...dogFragment + ...dogFragment + ...dogFragment + ...dogFragment + ...dogFragment + ...dogFragment + } + } + + fragment dogFragment on Dog { + name + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + } +} diff --git a/src/Core.Tests/Validation/FragmentsOnCompositeTypesRule.cs b/src/Core.Tests/Validation/FragmentsOnCompositeTypesRule.cs new file mode 100644 index 00000000000..926f258d495 --- /dev/null +++ b/src/Core.Tests/Validation/FragmentsOnCompositeTypesRule.cs @@ -0,0 +1,146 @@ +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class FragmentsOnCompositeTypesRuleTests + : ValidationTestBase + { + public FragmentsOnCompositeTypesRuleTests() + : base(new FragmentsOnCompositeTypesRule()) + { + } + + [Fact] + public void FragOnObject() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ... fragOnObject + } + } + + fragment fragOnObject on Dog { + name + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void FragOnInterface() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ... fragOnInterface + } + } + + fragment fragOnInterface on Pet { + name + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void FragOnUnion() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ... fragOnUnion + } + } + + fragment fragOnUnion on CatOrDog { + ... on Dog { + name + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void FragOnScalar() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ... fragOnScalar + } + } + + fragment fragOnScalar on Int { + something + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "Fragments can only be declared on unions, interfaces, " + + "and objects.")); + } + + [Fact] + public void InlineFragOnScalar() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ... inlineFragOnScalar + } + } + + fragment inlineFragOnScalar on Dog { + ... on Boolean { + somethingElse + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "Fragments can only be declared on unions, interfaces, " + + "and objects.")); + } + } +} diff --git a/src/Core.Tests/Validation/InputObjectFieldNamesRuleTests.cs b/src/Core.Tests/Validation/InputObjectFieldNamesRuleTests.cs new file mode 100644 index 00000000000..456147649c3 --- /dev/null +++ b/src/Core.Tests/Validation/InputObjectFieldNamesRuleTests.cs @@ -0,0 +1,79 @@ +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class InputObjectFieldNamesRuleTests + : ValidationTestBase + { + public InputObjectFieldNamesRuleTests() + : base(new InputObjectFieldNamesRule()) + { + } + + [Fact] + public void AllInputObjectFieldsExist() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + findDog(complex: { name: ""Fido"" }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + + [Fact] + public void InvalidInputObjectFieldsExist() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + findDog(complex: { favoriteCookieFlavor: ""Bacon"" }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "The specified input object field " + + "`favoriteCookieFlavor` does not exist.", + t.Message)); + } + + [Fact] + public void InvalidNestedInputObjectFieldsExist() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + findDog(complex: { child: { favoriteCookieFlavor: ""Bacon"" } }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "The specified input object field " + + "`favoriteCookieFlavor` does not exist.", + t.Message)); + } + } +} diff --git a/src/Core.Tests/Validation/InputObjectFieldUniquenessRuleTests.cs b/src/Core.Tests/Validation/InputObjectFieldUniquenessRuleTests.cs new file mode 100644 index 00000000000..c59e4e632a4 --- /dev/null +++ b/src/Core.Tests/Validation/InputObjectFieldUniquenessRuleTests.cs @@ -0,0 +1,55 @@ + +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class InputObjectFieldUniquenessRuleTests + : ValidationTestBase + { + public InputObjectFieldUniquenessRuleTests() + : base(new InputObjectFieldUniquenessRule()) + { + } + + [Fact] + public void NoFieldAmbiguity() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + findDog(complex: { name: ""A"", owner: ""B"" }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void NameFieldIsAmbiguous() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + findDog(complex: { name: ""A"", name: ""B"" }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "Field `name` is ambiguous.", + t.Message)); + } + } +} diff --git a/src/Core.Tests/Validation/InputObjectRequiredFieldsRuleTests.cs b/src/Core.Tests/Validation/InputObjectRequiredFieldsRuleTests.cs new file mode 100644 index 00000000000..06239187ce2 --- /dev/null +++ b/src/Core.Tests/Validation/InputObjectRequiredFieldsRuleTests.cs @@ -0,0 +1,116 @@ +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class InputObjectRequiredFieldsRuleTests + : ValidationTestBase + { + public InputObjectRequiredFieldsRuleTests() + : base(new InputObjectRequiredFieldsRule()) + { + } + + [Fact] + public void RequiredFieldsHaveValidValue() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + findDog2(complex: { name: ""Foo"" }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void NestedRequiredFieldsHaveValidValue() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + findDog2(complex: { name: ""Foo"" child: { name: ""123"" } }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void RequiredFieldIsNull() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + findDog2(complex: { name: null }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "`name` is a required field and cannot be null.", + t.Message)); + } + + [Fact] + public void RequiredFieldIsNotSet() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + findDog2(complex: { }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "`name` is a required field and cannot be null.", + t.Message)); + } + + [Fact] + public void NestedRequiredFieldIsNotSet() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + findDog2(complex: { name: ""foo"" child: { owner: ""bar"" } }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "`name` is a required field and cannot be null.", + t.Message)); + } + } +} diff --git a/src/Core.Tests/Validation/LeafFieldSelectionsRuleTests.cs b/src/Core.Tests/Validation/LeafFieldSelectionsRuleTests.cs new file mode 100644 index 00000000000..bcc634d4cf1 --- /dev/null +++ b/src/Core.Tests/Validation/LeafFieldSelectionsRuleTests.cs @@ -0,0 +1,148 @@ +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class LeafFieldSelectionsRuleTests + : ValidationTestBase + { + public LeafFieldSelectionsRuleTests() + : base(new LeafFieldSelectionsRule()) + { + } + + [Fact] + public void ScalarSelection() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + barkVolume + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void StringList() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + stringList + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void ScalarSelectionsNotAllowedOnInt() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + dog { + barkVolume { + sinceWhen + } + } + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "`barkVolume` is a scalar field. Selections on scalars " + + "or enums are never allowed, because they are the leaf " + + "nodes of any GraphQL query.")); + } + + [Fact] + public void DirectQueryOnObjectWithoutSubFields() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + query directQueryOnObjectWithoutSubFields { + human + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "`human` is an object, interface or union type " + + "field. Leaf selections on objects, interfaces, and " + + "unions without subfields are disallowed.")); + } + + [Fact] + public void DirectQueryOnInterfaceWithoutSubFields() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + query directQueryOnInterfaceWithoutSubFields { + pet + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "`pet` is an object, interface or union type " + + "field. Leaf selections on objects, interfaces, and " + + "unions without subfields are disallowed.")); + } + + [Fact] + public void DirectQueryOnUnionWithoutSubFields() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + query directQueryOnUnionWithoutSubFields { + catOrDog + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "`catOrDog` is an object, interface or union type " + + "field. Leaf selections on objects, interfaces, and " + + "unions without subfields are disallowed.")); + } + } +} diff --git a/src/Core.Tests/Validation/Models/ComplexInput.cs b/src/Core.Tests/Validation/Models/ComplexInput.cs index b6a9b5ef2c6..412c1ac44aa 100644 --- a/src/Core.Tests/Validation/Models/ComplexInput.cs +++ b/src/Core.Tests/Validation/Models/ComplexInput.cs @@ -5,5 +5,7 @@ public class ComplexInput public string Name { get; set; } public string Owner { get; set; } + + public ComplexInput Child { get; set; } } } diff --git a/src/Core.Tests/Validation/Models/ComplexInput2.cs b/src/Core.Tests/Validation/Models/ComplexInput2.cs new file mode 100644 index 00000000000..f506d17db91 --- /dev/null +++ b/src/Core.Tests/Validation/Models/ComplexInput2.cs @@ -0,0 +1,11 @@ +namespace HotChocolate.Validation +{ + public class ComplexInput2 + { + public string Name { get; set; } + + public string Owner { get; set; } + + public ComplexInput2 Child { get; set; } + } +} diff --git a/src/Core.Tests/Validation/Models/Dog.cs b/src/Core.Tests/Validation/Models/Dog.cs index b213e54aec9..003892f82fd 100644 --- a/src/Core.Tests/Validation/Models/Dog.cs +++ b/src/Core.Tests/Validation/Models/Dog.cs @@ -24,6 +24,4 @@ public Human GetOwner() return null; } } - - } diff --git a/src/Core.Tests/Validation/Models/Query.cs b/src/Core.Tests/Validation/Models/Query.cs index e6cc5760be6..00c644138dd 100644 --- a/src/Core.Tests/Validation/Models/Query.cs +++ b/src/Core.Tests/Validation/Models/Query.cs @@ -12,9 +12,34 @@ public Dog FindDog(ComplexInput complex) return null; } + public Dog FindDog2(ComplexInput2 complex) + { + return null; + } + public bool BooleanList(bool[] booleanListArg) { return true; } + + public Human GetHuman() + { + return null; + } + + public Human GetPet() + { + return null; + } + + public object GetCatOrDog() + { + return null; + } + + public string[] GetStringList() + { + return null; + } } } diff --git a/src/Core.Tests/Validation/QueryValidatorTests.cs b/src/Core.Tests/Validation/QueryValidatorTests.cs index ad07796ef90..fa48c961d99 100644 --- a/src/Core.Tests/Validation/QueryValidatorTests.cs +++ b/src/Core.Tests/Validation/QueryValidatorTests.cs @@ -7,7 +7,7 @@ namespace HotChocolate.Validation public class QueryValidatorTests { [Fact] - public void SchemaIsNulll() + public void SchemaIsNull() { // act Action a = () => new QueryValidator(null); @@ -496,7 +496,7 @@ public void UnusedFragment() "is not used within the current document.", t.Message)); } - [Fact] + [Fact] public void DuplicateFragments() { // arrange @@ -531,5 +531,331 @@ public void DuplicateFragments() "There are multiple fragments with the name `fragmentOne`.", t.Message)); } + + [Fact] + public void ScalarSelectionsNotAllowedOnInt() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + dog { + barkVolume { + sinceWhen + } + } + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "`barkVolume` is a scalar field. Selections on scalars " + + "or enums are never allowed, because they are the leaf " + + "nodes of any GraphQL query.")); + } + + [Fact] + public void InlineFragOnScalar() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ... inlineFragOnScalar + } + } + + fragment inlineFragOnScalar on Dog { + ... on Boolean { + somethingElse + } + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "Fragments can only be declared on unions, interfaces, " + + "and objects.")); + } + + [Fact] + public void FragmentCycle1() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...nameFragment + } + } + + fragment nameFragment on Dog { + name + ...barkVolumeFragment + } + + fragment barkVolumeFragment on Dog { + barkVolume + ...nameFragment + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The graph of fragment spreads must not form any " + + "cycles including spreading itself. Otherwise an " + + "operation could infinitely spread or infinitely " + + "execute on cycles in the underlying data.")); + } + + [Fact] + public void UndefinedFragment() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...undefinedFragment + } + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The specified fragment `undefinedFragment` does not exist.")); + } + + [Fact] + public void FragmentDoesNotMatchType() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...fragmentDoesNotMatchType + } + } + + fragment fragmentDoesNotMatchType on Human { + name + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The parent type does not match the type condition on " + + "the fragment `fragmentDoesNotMatchType`.")); + } + + [Fact] + public void NotExistingTypeOnInlineFragment() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + dog { + ...inlineNotExistingType + } + } + + fragment inlineNotExistingType on Dog { + ... on NotInSchema { + name + } + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal(t.Message, + "The specified inline fragment " + + "does not exist in the current schema.")); + } + + [Fact] + public void InvalidInputObjectFieldsExist() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + findDog(complex: { favoriteCookieFlavor: ""Bacon"" }) + { + name + } + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "The specified input object field " + + "`favoriteCookieFlavor` does not exist.", + t.Message)); + } + + [Fact] + public void RequiredFieldIsNull() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + findDog2(complex: { name: null }) + { + name + } + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "`name` is a required field and cannot be null.", + t.Message)); + } + + [Fact] + public void NameFieldIsAmbiguous() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + findDog(complex: { name: ""A"", name: ""B"" }) + { + name + } + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "Field `name` is ambiguous.", + t.Message)); + } + + [Fact] + public void UnsupportedDirective() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + dog { + name @foo(bar: true) + } + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "The specified directive `foo` " + + "is not supported by the current schema.", + t.Message)); + } + + [Fact] + public void StringIntoInt() + { + // arrange + DocumentNode query = Parser.Default.Parse(@" + { + arguments { + ...stringIntoInt + } + } + + fragment stringIntoInt on Arguments { + intArgField(intArg: ""123"") + } + "); + + Schema schema = ValidationUtils.CreateSchema(); + var queryValidator = new QueryValidator(schema); + + // act + QueryValidationResult result = queryValidator.Validate(query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "The specified value type of argument `intArg` " + + "does not match the argument type.", + t.Message)); + } } } diff --git a/src/Core.Tests/Validation/Types/ComplexInput2Type.cs b/src/Core.Tests/Validation/Types/ComplexInput2Type.cs new file mode 100644 index 00000000000..df71cf9fffa --- /dev/null +++ b/src/Core.Tests/Validation/Types/ComplexInput2Type.cs @@ -0,0 +1,19 @@ +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + public class ComplexInput2Type + : InputObjectType + { + protected override void Configure( + IInputObjectTypeDescriptor descriptor) + { + descriptor.Field(t => t.Name) + .Type>(); + descriptor.Field(t => t.Owner) + .Type>() + .DefaultValue(new StringValueNode("1234")); + } + } +} diff --git a/src/Core.Tests/Validation/Types/HumanType.cs b/src/Core.Tests/Validation/Types/HumanType.cs index 2092df2eee8..0f0ffa98fc8 100644 --- a/src/Core.Tests/Validation/Types/HumanType.cs +++ b/src/Core.Tests/Validation/Types/HumanType.cs @@ -9,6 +9,9 @@ protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor.Interface(); descriptor.Field(t => t.Name).Type>(); + descriptor.Field("pets") + .Type>() + .Resolver(() => ""); } } } diff --git a/src/Core.Tests/Validation/Types/QueryType.cs b/src/Core.Tests/Validation/Types/QueryType.cs index 5abda2957ba..2d1230af79b 100644 --- a/src/Core.Tests/Validation/Types/QueryType.cs +++ b/src/Core.Tests/Validation/Types/QueryType.cs @@ -10,6 +10,9 @@ protected override void Configure(IObjectTypeDescriptor descriptor) descriptor.Field("arguments") .Type() .Resolver(() => null); + + descriptor.Field(t => t.GetCatOrDog()) + .Type(); } } } diff --git a/src/Core.Tests/Validation/ValidationUtils.cs b/src/Core.Tests/Validation/ValidationUtils.cs index 4ca9327703d..e9115886a44 100644 --- a/src/Core.Tests/Validation/ValidationUtils.cs +++ b/src/Core.Tests/Validation/ValidationUtils.cs @@ -18,6 +18,7 @@ public static Schema CreateSchema() c.RegisterType(); c.RegisterSubscriptionType(); c.RegisterType(); + c.RegisterType(); }); } } diff --git a/src/Core.Tests/Validation/ValuesOfCorrectTypeRuleTests.cs b/src/Core.Tests/Validation/ValuesOfCorrectTypeRuleTests.cs new file mode 100644 index 00000000000..492bf7329c1 --- /dev/null +++ b/src/Core.Tests/Validation/ValuesOfCorrectTypeRuleTests.cs @@ -0,0 +1,179 @@ +using HotChocolate.Language; +using Xunit; + +namespace HotChocolate.Validation +{ + public class ValuesOfCorrectTypeRuleTests + : ValidationTestBase + { + public ValuesOfCorrectTypeRuleTests() + : base(new ValuesOfCorrectTypeRule()) + { + } + + [Fact] + public void GoodBooleanArg() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + arguments { + ...goodBooleanArg + } + } + + fragment goodBooleanArg on Arguments { + booleanArgField(booleanArg: true) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void CoercedIntIntoFloatArg() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + arguments { + ...coercedIntIntoFloatArg + } + } + + fragment coercedIntIntoFloatArg on Arguments { + # Note: The input coercion rules for Float allow Int literals. + floatArgField(floatArg: 123) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void GoodComplexDefaultValue() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + query goodComplexDefaultValue($search: ComplexInput = { name: ""Fido"" }) { + findDog(complex: $search) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.False(result.HasErrors); + } + + [Fact] + public void StringIntoInt() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + { + arguments { + ...stringIntoInt + } + } + + fragment stringIntoInt on Arguments { + intArgField(intArg: ""123"") + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "The specified value type of argument `intArg` " + + "does not match the argument type.", + t.Message)); + } + + [Fact] + public void BadComplexValueArgument() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + query badComplexValue { + findDog(complex: { name: 123 }) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "The specified value type of field `name` " + + "does not match the field type.", + t.Message)); + } + + [Fact] + public void BadComplexValueVariable() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + query goodComplexDefaultValue($search: ComplexInput = { name: 123 }) { + findDog(complex: $search) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "The specified value type of field `name` " + + "does not match the field type.", + t.Message)); + } + + [Fact] + public void BadValueVariable() + { + // arrange + Schema schema = ValidationUtils.CreateSchema(); + DocumentNode query = Parser.Default.Parse(@" + query goodComplexDefaultValue($search: ComplexInput = 123) { + findDog(complex: $search) + } + "); + + // act + QueryValidationResult result = Rule.Validate(schema, query); + + // assert + Assert.True(result.HasErrors); + Assert.Collection(result.Errors, + t => Assert.Equal( + "The specified value type of variable `search` " + + "does not match the variable type.", + t.Message)); + } + } +} diff --git a/src/Core.Tests/__snapshots__/ConditionalInlineFragment.json b/src/Core.Tests/__snapshots__/ConditionalInlineFragment.json new file mode 100644 index 00000000000..034542c970d --- /dev/null +++ b/src/Core.Tests/__snapshots__/ConditionalInlineFragment.json @@ -0,0 +1,11 @@ +{ + "Data": { + "heroes": [ + { + "name": "Luke Skywalker", + "height": 1.72 + } + ] + }, + "Errors": null +} diff --git a/src/Core/Execution/FieldCollector.cs b/src/Core/Execution/FieldCollector.cs index ea5478869a9..e66058550c0 100644 --- a/src/Core/Execution/FieldCollector.cs +++ b/src/Core/Execution/FieldCollector.cs @@ -96,7 +96,7 @@ internal class FieldCollector } else if (selection is InlineFragmentNode inlineFragment) { - Fragment fragment = _fragments.GetFragment(inlineFragment); + Fragment fragment = _fragments.GetFragment(type, inlineFragment); if (DoesFragmentTypeApply(type, fragment.TypeCondition)) { CollectFields(type, fragment.SelectionSet, reportError, fields); diff --git a/src/Core/Execution/FragmentCollection.cs b/src/Core/Execution/FragmentCollection.cs index ecd2603d5cb..17bab137976 100644 --- a/src/Core/Execution/FragmentCollection.cs +++ b/src/Core/Execution/FragmentCollection.cs @@ -62,34 +62,54 @@ private IEnumerable CreateFragments(string fragmentName) } } - public Fragment GetFragment(InlineFragmentNode inlineFragment) + public Fragment GetFragment( + ObjectType parentType, + InlineFragmentNode inlineFragment) { + if (parentType == null) + { + throw new ArgumentNullException(nameof(parentType)); + } + if (inlineFragment == null) { throw new ArgumentNullException(nameof(inlineFragment)); } string fragmentName = CreateInlineFragmentName(inlineFragment); + if (!_fragments.TryGetValue(fragmentName, out List fragments)) { fragments = new List(); - fragments.Add(CreateFragment(inlineFragment)); + fragments.Add(CreateFragment(parentType, inlineFragment)); _fragments[fragmentName] = fragments; } return fragments.First(); } - private Fragment CreateFragment(InlineFragmentNode inlineFragment) + private Fragment CreateFragment( + ObjectType parentType, + InlineFragmentNode inlineFragment) { - // TODO : maybe introduce a tryget to the schema - INamedType type = _schema.GetType( - inlineFragment.TypeCondition.Name.Value); + INamedType type; + + if (inlineFragment.TypeCondition == null) + { + type = parentType; + } + else + { + type = _schema.GetType( + inlineFragment.TypeCondition.Name.Value); + } + return new Fragment(type, inlineFragment.SelectionSet); } - private string CreateInlineFragmentName(InlineFragmentNode inlineFragment) + private string CreateInlineFragmentName( + InlineFragmentNode inlineFragment) { return $"^__{inlineFragment.Location.Start}_{inlineFragment.Location.End}"; } diff --git a/src/Core/Validation/DirectivesAreDefinedRule.cs b/src/Core/Validation/DirectivesAreDefinedRule.cs new file mode 100644 index 00000000000..5aab40e070a --- /dev/null +++ b/src/Core/Validation/DirectivesAreDefinedRule.cs @@ -0,0 +1,20 @@ +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + /// + /// GraphQL servers define what directives they support. + /// For each usage of a directive, the directive must be available + /// on that server. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Directives-Are-Defined + /// + internal sealed class DirectivesAreDefinedRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new DirectivesAreDefinedVisitor(schema); + } + } +} diff --git a/src/Core/Validation/DirectivesAreDefinedVisitor.cs b/src/Core/Validation/DirectivesAreDefinedVisitor.cs new file mode 100644 index 00000000000..b632e4e1fe2 --- /dev/null +++ b/src/Core/Validation/DirectivesAreDefinedVisitor.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using HotChocolate.Language; + +namespace HotChocolate.Validation +{ + internal sealed class DirectivesAreDefinedVisitor + : QueryVisitorErrorBase + { + private readonly HashSet _directives; + + public DirectivesAreDefinedVisitor(ISchema schema) + : base(schema) + { + _directives = new HashSet( + schema.Directives.Select(t => t.Name)); + } + + protected override void VisitDirective( + DirectiveNode directive, + ImmutableStack path) + { + if (!_directives.Contains(directive.Name.Value)) + { + Errors.Add(new ValidationError( + $"The specified directive `{directive.Name.Value}` " + + "is not supported by the current schema.", + directive)); + } + } + } +} diff --git a/src/Core/Validation/DirectivesAreInValidLocationsRule.cs b/src/Core/Validation/DirectivesAreInValidLocationsRule.cs index 75ec3a869a1..24909de485a 100644 --- a/src/Core/Validation/DirectivesAreInValidLocationsRule.cs +++ b/src/Core/Validation/DirectivesAreInValidLocationsRule.cs @@ -3,6 +3,15 @@ namespace HotChocolate.Validation { + /// + ///GraphQL servers define what directives they support and where they + /// support them. + /// + /// For each usage of a directive, the directive must be used in a + /// location that the server has declared support for. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Directives-Are-In-Valid-Locations + /// internal sealed class DirectivesAreInValidLocationsRule : QueryVisitorValidationErrorBase { diff --git a/src/Core/Validation/FragmentSpreadIsPossibleRule.cs b/src/Core/Validation/FragmentSpreadIsPossibleRule.cs new file mode 100644 index 00000000000..8d4f0c83390 --- /dev/null +++ b/src/Core/Validation/FragmentSpreadIsPossibleRule.cs @@ -0,0 +1,22 @@ +namespace HotChocolate.Validation +{ + /// + /// Fragments are declared on a type and will only apply when the + /// runtime object type matches the type condition. + /// + /// They also are spread within the context of a parent type. + /// + /// A fragment spread is only valid if its type condition could ever + /// apply within the parent type. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Fragment-spread-is-possible + /// + internal sealed class FragmentSpreadIsPossibleRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new FragmentSpreadIsPossibleVisitor(schema); + } + } +} diff --git a/src/Core/Validation/FragmentSpreadIsPossibleVisitor.cs b/src/Core/Validation/FragmentSpreadIsPossibleVisitor.cs new file mode 100644 index 00000000000..afe7ac06cd9 --- /dev/null +++ b/src/Core/Validation/FragmentSpreadIsPossibleVisitor.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal sealed class FragmentSpreadIsPossibleVisitor + : QueryVisitorErrorBase + { + public FragmentSpreadIsPossibleVisitor(ISchema schema) + : base(schema) + { + } + + protected override void VisitFragmentSpread( + FragmentSpreadNode fragmentSpread, + IType type, + ImmutableStack path) + { + if (type is INamedType parentType + && TryGetFragment(fragmentSpread.Name.Value, + out FragmentDefinitionNode fragment) + && Schema.TryGetType(fragment.TypeCondition.Name.Value, + out INamedOutputType typeCondition) + && parentType.IsCompositeType() + && typeCondition.IsCompositeType() + && !GetPossibleType(parentType) + .Intersect(GetPossibleType(typeCondition)) + .Any()) + { + Errors.Add(new ValidationError( + "The parent type does not match the type condition on " + + $"the fragment `{fragment.Name}`.", fragmentSpread)); + } + + base.VisitFragmentSpread(fragmentSpread, type, path); + } + + private IEnumerable GetPossibleType(INamedType type) + { + if (type is ObjectType ot) + { + return new[] { ot }; + } + + return Schema.GetPossibleTypes(type); + } + } +} diff --git a/src/Core/Validation/FragmentSpreadTargetDefinedRule.cs b/src/Core/Validation/FragmentSpreadTargetDefinedRule.cs new file mode 100644 index 00000000000..a6bebc0ed97 --- /dev/null +++ b/src/Core/Validation/FragmentSpreadTargetDefinedRule.cs @@ -0,0 +1,21 @@ +using System.Linq; + +namespace HotChocolate.Validation +{ + /// + /// Named fragment spreads must refer to fragments defined within the + /// document. + /// + /// It is a validation error if the target of a spread is not defined. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Fragment-spread-target-defined + /// + internal sealed class FragmentSpreadTargetDefinedRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new FragmentSpreadTargetDefinedVisitor(schema); + } + } +} diff --git a/src/Core/Validation/FragmentSpreadTargetDefinedVisitor.cs b/src/Core/Validation/FragmentSpreadTargetDefinedVisitor.cs new file mode 100644 index 00000000000..9ce70c7f5b3 --- /dev/null +++ b/src/Core/Validation/FragmentSpreadTargetDefinedVisitor.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal sealed class FragmentSpreadTargetDefinedVisitor + : QueryVisitorErrorBase + { + private readonly Dictionary> _missingFragments = + new Dictionary>(); + + public FragmentSpreadTargetDefinedVisitor(ISchema schema) + : base(schema) + { + } + + protected override void VisitDocument( + DocumentNode document, + ImmutableStack path) + { + base.VisitDocument(document, path); + + foreach (KeyValuePair> item in + _missingFragments) + { + Errors.Add(new ValidationError( + $"The specified fragment `{item.Key}` does not exist.", + item.Value)); + } + } + + protected override void VisitFragmentSpread( + FragmentSpreadNode fragmentSpread, + IType type, + ImmutableStack path) + { + if (!ContainsFragment(fragmentSpread.Name.Value)) + { + string fragmentName = fragmentSpread.Name.Value; + if (!_missingFragments.TryGetValue(fragmentName, + out List f)) + { + f = new List(); + _missingFragments[fragmentName] = f; + } + f.Add(fragmentSpread); + } + + base.VisitFragmentSpread(fragmentSpread, type, path); + } + } +} diff --git a/src/Core/Validation/FragmentSpreadTypeExistenceRule.cs b/src/Core/Validation/FragmentSpreadTypeExistenceRule.cs new file mode 100644 index 00000000000..34c725add26 --- /dev/null +++ b/src/Core/Validation/FragmentSpreadTypeExistenceRule.cs @@ -0,0 +1,18 @@ +namespace HotChocolate.Validation +{ + /// + /// Fragments must be specified on types that exist in the schema. + /// This applies for both named and inline fragments. + /// If they are not defined in the schema, the query does not validate. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Fragment-Spread-Type-Existence + /// + internal sealed class FragmentSpreadTypeExistenceRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new FragmentSpreadTypeExistenceVisitor(schema); + } + } +} diff --git a/src/Core/Validation/FragmentSpreadTypeExistenceVisitor.cs b/src/Core/Validation/FragmentSpreadTypeExistenceVisitor.cs new file mode 100644 index 00000000000..374321e7911 --- /dev/null +++ b/src/Core/Validation/FragmentSpreadTypeExistenceVisitor.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal sealed class FragmentSpreadTypeExistenceVisitor + : QueryVisitorErrorBase + { + public FragmentSpreadTypeExistenceVisitor(ISchema schema) + : base(schema) + { + } + + protected override void VisitFragmentDefinition( + FragmentDefinitionNode fragmentDefinition, + ImmutableStack path) + { + if (!IsFragmentVisited(fragmentDefinition) + && (fragmentDefinition.TypeCondition?.Name?.Value == null + || !Schema.TryGetType( + fragmentDefinition.TypeCondition.Name.Value, + out INamedOutputType typeCondition))) + { + Errors.Add(new ValidationError( + $"The type of fragment `{fragmentDefinition.Name.Value}` " + + "does not exist in the current schema.", + fragmentDefinition)); + } + + base.VisitFragmentDefinition(fragmentDefinition, path); + } + + protected override void VisitInlineFragment( + InlineFragmentNode inlineFragment, + IType parentType, + IType typeCondition, + ImmutableStack path) + { + if (typeCondition == null) + { + Errors.Add(new ValidationError( + "The specified inline fragment " + + "does not exist in the current schema.", + inlineFragment)); + } + + base.VisitInlineFragment( + inlineFragment, + parentType, + typeCondition, + path); + } + } +} diff --git a/src/Core/Validation/FragmentSpreadsMustNotFormCyclesRule.cs b/src/Core/Validation/FragmentSpreadsMustNotFormCyclesRule.cs new file mode 100644 index 00000000000..26db8b76347 --- /dev/null +++ b/src/Core/Validation/FragmentSpreadsMustNotFormCyclesRule.cs @@ -0,0 +1,18 @@ +namespace HotChocolate.Validation +{ + /// + /// The graph of fragment spreads must not form any cycles including + /// spreading itself. Otherwise an operation could infinitely spread or + /// infinitely execute on cycles in the underlying data. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Fragment-spreads-must-not-form-cycles + /// + internal sealed class FragmentSpreadsMustNotFormCyclesRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new FragmentSpreadsMustNotFormCyclesVisitor(schema); + } + } +} diff --git a/src/Core/Validation/FragmentSpreadsMustNotFormCyclesVisitor.cs b/src/Core/Validation/FragmentSpreadsMustNotFormCyclesVisitor.cs new file mode 100644 index 00000000000..dec92841631 --- /dev/null +++ b/src/Core/Validation/FragmentSpreadsMustNotFormCyclesVisitor.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal sealed class FragmentSpreadsMustNotFormCyclesVisitor + : QueryVisitorErrorBase + { + private readonly HashSet _visited = + new HashSet(); + + private bool _cycleDetected; + + public FragmentSpreadsMustNotFormCyclesVisitor(ISchema schema) + : base(schema) + { + } + + protected override void VisitOperationDefinitions( + IEnumerable oprationDefinitions, + ImmutableStack path) + { + foreach (OperationDefinitionNode operation in oprationDefinitions) + { + _visited.Clear(); + VisitOperationDefinition(operation, path); + if (_cycleDetected) + { + return; + } + } + } + + protected override void VisitFragmentDefinitions( + IEnumerable fragmentDefinitions, + ImmutableStack path) + { + // we do not want do visit any fragments separately. + } + + protected override void VisitFragmentSpread( + FragmentSpreadNode fragmentSpread, + IType type, + ImmutableStack path) + { + if (_cycleDetected) + { + return; + } + + ImmutableStack newpath = path.Push(fragmentSpread); + + if (path.Last() is DocumentNode d) + { + string fragmentName = fragmentSpread.Name.Value; + if (TryGetFragment(fragmentName, + out FragmentDefinitionNode fragment)) + { + VisitFragmentDefinition(fragment, newpath); + } + } + + VisitDirectives(fragmentSpread.Directives, newpath); + } + + protected override void VisitFragmentDefinition( + FragmentDefinitionNode fragmentDefinition, + ImmutableStack path) + { + if (_cycleDetected) + { + return; + } + + if (fragmentDefinition.TypeCondition?.Name?.Value != null + && Schema.TryGetType( + fragmentDefinition.TypeCondition.Name.Value, + out INamedOutputType typeCondition)) + { + if (_visited.Add(fragmentDefinition)) + { + ImmutableStack newpath = path + .Push(fragmentDefinition); + + VisitSelectionSet( + fragmentDefinition.SelectionSet, + typeCondition, + newpath); + + VisitDirectives( + fragmentDefinition.Directives, + newpath); + } + else + { + DetectCycle(fragmentDefinition, path); + } + } + } + + private void DetectCycle( + FragmentDefinitionNode fragmentDefinition, + ImmutableStack path) + { + ImmutableStack current = path; + + while (current.Any()) + { + current = current.Pop(out ISyntaxNode node); + + if (node == fragmentDefinition) + { + _cycleDetected = true; + Errors.Add(new ValidationError( + "The graph of fragment spreads must not form any " + + "cycles including spreading itself. Otherwise an " + + "operation could infinitely spread or infinitely " + + "execute on cycles in the underlying data.", + GetCyclePath(path))); + return; + } + } + } + + private IEnumerable GetCyclePath( + ImmutableStack path) + { + ImmutableStack current = path; + + while (current.Any()) + { + current = current.Pop(out ISyntaxNode node); + + if (node is FragmentSpreadNode) + { + yield return node; + } + } + } + } +} diff --git a/src/Core/Validation/FragmentsOnCompositeTypesRule.cs b/src/Core/Validation/FragmentsOnCompositeTypesRule.cs new file mode 100644 index 00000000000..3fbaf02c66e --- /dev/null +++ b/src/Core/Validation/FragmentsOnCompositeTypesRule.cs @@ -0,0 +1,19 @@ +namespace HotChocolate.Validation +{ + /// + /// Fragments can only be declared on unions, interfaces, and objects. + /// They are invalid on scalars. + /// They can only be applied on non‐leaf fields. + /// This rule applies to both inline and named fragments. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Fragments-On-Composite-Types + /// + internal sealed class FragmentsOnCompositeTypesRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new FragmentsOnCompositeTypesVisitor(schema); + } + } +} diff --git a/src/Core/Validation/FragmentsOnCompositeTypesVisitor.cs b/src/Core/Validation/FragmentsOnCompositeTypesVisitor.cs new file mode 100644 index 00000000000..ee6f81e6716 --- /dev/null +++ b/src/Core/Validation/FragmentsOnCompositeTypesVisitor.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal sealed class FragmentsOnCompositeTypesVisitor + : QueryVisitorErrorBase + { + private readonly List _fragmentErrors = + new List(); + + public FragmentsOnCompositeTypesVisitor(ISchema schema) + : base(schema) + { + } + + protected override void VisitDocument( + DocumentNode document, + ImmutableStack path) + { + _fragmentErrors.Clear(); + + base.VisitDocument(document, path); + + if (_fragmentErrors.Count > 0) + { + Errors.Add(new ValidationError( + "Fragments can only be declared on unions, interfaces, " + + "and objects.", _fragmentErrors)); + } + } + + protected override void VisitFragmentDefinition( + FragmentDefinitionNode fragmentDefinition, + ImmutableStack path) + { + ValidateTypeCondition( + fragmentDefinition, + fragmentDefinition.TypeCondition.Name.Value); + + base.VisitFragmentDefinition(fragmentDefinition, path); + } + + protected override void VisitInlineFragment( + InlineFragmentNode inlineFragment, + IType parentType, + IType typeCondition, + ImmutableStack path) + { + ValidateTypeCondition( + inlineFragment, + typeCondition); + + base.VisitInlineFragment( + inlineFragment, + parentType, + typeCondition, + path); + } + + private void ValidateTypeCondition( + ISyntaxNode syntaxNode, + string typeCondition) + { + if (typeCondition != null + && Schema.TryGetType(typeCondition, out INamedType type)) + { + ValidateTypeCondition(syntaxNode, type); + } + } + + private void ValidateTypeCondition( + ISyntaxNode syntaxNode, + IType typeCondition) + { + if (typeCondition != null + && !typeCondition.IsCompositeType()) + { + _fragmentErrors.Add(syntaxNode); + } + } + } +} diff --git a/src/Core/Validation/InputObjectFieldNamesRule.cs b/src/Core/Validation/InputObjectFieldNamesRule.cs new file mode 100644 index 00000000000..5a95df82c53 --- /dev/null +++ b/src/Core/Validation/InputObjectFieldNamesRule.cs @@ -0,0 +1,17 @@ +namespace HotChocolate.Validation +{ + /// + /// Every input field provided in an input object value must be defined in + /// the set of possible fields of that input object’s expected type. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Input-Object-Field-Names + /// + internal sealed class InputObjectFieldNamesRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new InputObjectFieldNamesVisitor(schema); + } + } +} diff --git a/src/Core/Validation/InputObjectFieldNamesVisitor.cs b/src/Core/Validation/InputObjectFieldNamesVisitor.cs new file mode 100644 index 00000000000..c557ed89156 --- /dev/null +++ b/src/Core/Validation/InputObjectFieldNamesVisitor.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal sealed class InputObjectFieldNamesVisitor + : InputObjectFieldVisitorBase + { + private readonly HashSet _visited = + new HashSet(); + + public InputObjectFieldNamesVisitor(ISchema schema) + : base(schema) + { + } + + protected override void VisitObjectValue( + InputObjectType type, + ObjectValueNode objectValue) + { + if (_visited.Add(objectValue)) + { + foreach (ObjectFieldNode fieldValue in objectValue.Fields) + { + if (type.Fields.TryGetField(fieldValue.Name.Value, + out InputField inputField)) + { + if (inputField.Type is InputObjectType inputFieldType + && fieldValue.Value is ObjectValueNode ov) + { + VisitObjectValue(inputFieldType, ov); + } + } + else + { + Errors.Add(new ValidationError( + "The specified input object field " + + $"`{fieldValue.Name.Value}` does not exist.", + fieldValue)); + } + } + } + } + } +} diff --git a/src/Core/Validation/InputObjectFieldUniquenessRule.cs b/src/Core/Validation/InputObjectFieldUniquenessRule.cs new file mode 100644 index 00000000000..a9b7f650fdd --- /dev/null +++ b/src/Core/Validation/InputObjectFieldUniquenessRule.cs @@ -0,0 +1,18 @@ +namespace HotChocolate.Validation +{ + /// + /// Input objects must not contain more than one field of the same name, + /// otherwise an ambiguity would exist which includes an ignored portion + /// of syntax. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Input-Object-Field-Uniqueness + /// + internal sealed class InputObjectFieldUniquenessRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new InputObjectFieldUniquenessVisitor(schema); + } + } +} diff --git a/src/Core/Validation/InputObjectFieldUniquenessVisitor.cs b/src/Core/Validation/InputObjectFieldUniquenessVisitor.cs new file mode 100644 index 00000000000..349083a8114 --- /dev/null +++ b/src/Core/Validation/InputObjectFieldUniquenessVisitor.cs @@ -0,0 +1,59 @@ + +using System.Collections.Generic; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal sealed class InputObjectFieldUniquenessVisitor + : InputObjectFieldVisitorBase + { + private readonly HashSet _visited = + new HashSet(); + + public InputObjectFieldUniquenessVisitor(ISchema schema) + : base(schema) + { + } + + protected override void VisitObjectValue( + InputObjectType type, + ObjectValueNode objectValue) + { + var visitedFields = new HashSet(); + + if (_visited.Add(objectValue)) + { + foreach (ObjectFieldNode fieldValue in objectValue.Fields) + { + if (type.Fields.TryGetField(fieldValue.Name.Value, + out InputField field)) + { + VisitInputField(visitedFields, field, fieldValue); + } + } + } + } + + private void VisitInputField( + HashSet visitedFields, + InputField field, + ObjectFieldNode fieldValue) + { + if (visitedFields.Add(field.Name)) + { + if (fieldValue.Value is ObjectValueNode ov + && field.Type.NamedType() is InputObjectType it) + { + VisitObjectValue(it, ov); + } + } + else + { + Errors.Add(new ValidationError( + $"Field `{field.Name}` is ambiguous.", + fieldValue)); + } + } + } +} diff --git a/src/Core/Validation/InputObjectFieldVisitorBase.cs b/src/Core/Validation/InputObjectFieldVisitorBase.cs new file mode 100644 index 00000000000..881825d8d30 --- /dev/null +++ b/src/Core/Validation/InputObjectFieldVisitorBase.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal abstract class InputObjectFieldVisitorBase + : QueryVisitorErrorBase + { + private readonly Dictionary _directives; + + protected InputObjectFieldVisitorBase(ISchema schema) + : base(schema) + { + _directives = schema.Directives.ToDictionary(t => t.Name); + } + + protected override void VisitField( + FieldNode field, + IType type, + ImmutableStack path) + { + if (type is IComplexOutputType ct + && ct.Fields.TryGetField(field.Name.Value, out IOutputField f)) + { + VisitArguments(field.Arguments, f.Arguments); + } + } + + protected override void VisitDirective( + DirectiveNode directive, + ImmutableStack path) + { + if (_directives.TryGetValue(directive.Name.Value, out Directive d)) + { + VisitArguments(directive.Arguments, d.Arguments); + } + } + + private void VisitArguments( + IEnumerable arguments, + IFieldCollection argumentFields) + { + foreach (ArgumentNode argument in arguments) + { + if (argument.Value is ObjectValueNode ov + && argumentFields.TryGetField(argument.Name.Value, + out IInputField argumentField) + && argumentField.Type.NamedType() is InputObjectType io) + { + VisitObjectValue(io, ov); + } + } + } + + protected abstract void VisitObjectValue( + InputObjectType type, + ObjectValueNode objectValue); + } +} diff --git a/src/Core/Validation/InputObjectRequiredFieldsRule.cs b/src/Core/Validation/InputObjectRequiredFieldsRule.cs new file mode 100644 index 00000000000..96b53086b77 --- /dev/null +++ b/src/Core/Validation/InputObjectRequiredFieldsRule.cs @@ -0,0 +1,20 @@ +namespace HotChocolate.Validation +{ + /// + /// Input object fields may be required. Much like a field may have + /// required arguments, an input object may have required fields. + /// + /// An input field is required if it has a non‐null type and does not have + /// a default value. Otherwise, the input object field is optional. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Input-Object-Required-Fields + /// + internal sealed class InputObjectRequiredFieldsRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new InputObjectRequiredFieldsVisitor(schema); + } + } +} diff --git a/src/Core/Validation/InputObjectRequiredFieldsVisitor.cs b/src/Core/Validation/InputObjectRequiredFieldsVisitor.cs new file mode 100644 index 00000000000..438260514f6 --- /dev/null +++ b/src/Core/Validation/InputObjectRequiredFieldsVisitor.cs @@ -0,0 +1,76 @@ + +using System.Collections.Generic; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal sealed class InputObjectRequiredFieldsVisitor + : InputObjectFieldVisitorBase + { + private readonly HashSet _visited = + new HashSet(); + + public InputObjectRequiredFieldsVisitor(ISchema schema) + : base(schema) + { + } + + protected override void VisitObjectValue( + InputObjectType type, + ObjectValueNode objectValue) + { + if (_visited.Add(objectValue)) + { + Dictionary fieldValues = + CreateFieldMap(objectValue); + + foreach (InputField field in type.Fields) + { + fieldValues.TryGetValue(field.Name, + out ObjectFieldNode fieldValue); + + ValidateInputField(field, fieldValue, + (ISyntaxNode)fieldValue ?? objectValue); + + if (fieldValue?.Value is ObjectValueNode ov + && field.Type.NamedType() is InputObjectType it) + { + VisitObjectValue(it, ov); + } + } + } + } + + private void ValidateInputField( + InputField field, + ObjectFieldNode fieldValue, + ISyntaxNode node) + { + if (field.Type.IsNonNullType() + && field.DefaultValue.IsNull() + && ValueNodeExtensions.IsNull(fieldValue?.Value)) + { + Errors.Add(new ValidationError( + $"`{field.Name}` is a required field and cannot be null.", + node)); + } + } + + private Dictionary CreateFieldMap( + ObjectValueNode objectValue) + { + var fields = new Dictionary(); + + foreach (ObjectFieldNode fieldValue in objectValue.Fields) + { + if (!fields.ContainsKey(fieldValue.Name.Value)) + { + fields[fieldValue.Name.Value] = fieldValue; + } + } + + return fields; + } + } +} diff --git a/src/Core/Validation/LeafFieldSelectionsRule.cs b/src/Core/Validation/LeafFieldSelectionsRule.cs new file mode 100644 index 00000000000..1a55c35d8f7 --- /dev/null +++ b/src/Core/Validation/LeafFieldSelectionsRule.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace HotChocolate.Validation +{ + /// + /// Field selections on scalars or enums are never allowed, + /// because they are the leaf nodes of any GraphQL query. + /// + /// Conversely the leaf field selections of GraphQL queries + /// must be of type scalar or enum. Leaf selections on objects, + /// interfaces, and unions without subfields are disallowed. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Leaf-Field-Selections + /// + internal sealed class LeafFieldSelectionsRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new LeafFieldSelectionsVisitor(schema); + } + } +} diff --git a/src/Core/Validation/LeafFieldSelectionsVisitor.cs b/src/Core/Validation/LeafFieldSelectionsVisitor.cs new file mode 100644 index 00000000000..7a91b08c627 --- /dev/null +++ b/src/Core/Validation/LeafFieldSelectionsVisitor.cs @@ -0,0 +1,64 @@ +using System.Collections.Immutable; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal sealed class LeafFieldSelectionsVisitor + : QueryVisitorErrorBase + { + public LeafFieldSelectionsVisitor(ISchema schema) + : base(schema) + { + } + + protected override void VisitField( + FieldNode field, + IType type, + ImmutableStack path) + { + if (type is IComplexOutputType t + && t.Fields.TryGetField(field.Name.Value, out IOutputField f)) + { + if (f.Type.NamedType().IsScalarType() + || f.Type.NamedType().IsEnumType()) + { + ValidateLeafField(field, f); + } + else + { + ValidateNodeField(field, f); + base.VisitField(field, type, path); + } + } + } + + private void ValidateLeafField( + FieldNode fieldSelection, + IOutputField field) + { + if (fieldSelection.SelectionSet != null) + { + string type = field.Type.IsScalarType() ? "a scalar" : "an enum"; + Errors.Add(new ValidationError( + $"`{field.Name}` is {type} field. Selections on scalars " + + "or enums are never allowed, because they are the leaf " + + "nodes of any GraphQL query.", fieldSelection)); + } + } + + private void ValidateNodeField( + FieldNode fieldSelection, + IOutputField field) + { + if (fieldSelection.SelectionSet == null) + { + Errors.Add(new ValidationError( + $"`{field.Name}` is an object, interface or union type " + + "field. Leaf selections on objects, interfaces, and " + + "unions without subfields are disallowed.", + fieldSelection)); + } + } + } +} diff --git a/src/Core/Validation/QueryValidator.cs b/src/Core/Validation/QueryValidator.cs index 5a271777b32..3939a1ec686 100644 --- a/src/Core/Validation/QueryValidator.cs +++ b/src/Core/Validation/QueryValidator.cs @@ -26,6 +26,17 @@ public class QueryValidator new ArgumentNamesRule(), new FragmentsMustBeUsedRule(), new FragmentNameUniquenessRule(), + new LeafFieldSelectionsRule(), + new FragmentsOnCompositeTypesRule(), + new FragmentSpreadsMustNotFormCyclesRule(), + new FragmentSpreadTargetDefinedRule(), + new FragmentSpreadIsPossibleRule(), + new FragmentSpreadTypeExistenceRule(), + new InputObjectFieldNamesRule(), + new InputObjectRequiredFieldsRule(), + new InputObjectFieldUniquenessRule(), + new DirectivesAreDefinedRule(), + new ValuesOfCorrectTypeRule() }; private readonly ISchema _schema; diff --git a/src/Core/Validation/QueryVisitor.cs b/src/Core/Validation/QueryVisitor.cs index 92b17cd9e71..296c9becff4 100644 --- a/src/Core/Validation/QueryVisitor.cs +++ b/src/Core/Validation/QueryVisitor.cs @@ -11,8 +11,10 @@ internal class QueryVisitor { private readonly Dictionary _fragments = new Dictionary(); - private readonly HashSet _visitedFragments = - new HashSet(); + private readonly HashSet _visitedFragments = + new HashSet(); + private readonly HashSet _touchedFragments = + new HashSet(); protected QueryVisitor(ISchema schema) { @@ -39,7 +41,6 @@ public void VisitDocument(DocumentNode document) VisitDocument(document, path); } - protected virtual void VisitDocument( DocumentNode document, ImmutableStack path) @@ -67,7 +68,9 @@ public void VisitDocument(DocumentNode document) IEnumerable fragmentDefinitions, ImmutableStack path) { - foreach (FragmentDefinitionNode fragment in fragmentDefinitions) + foreach (FragmentDefinitionNode fragment in + fragmentDefinitions.Where( + t => !_touchedFragments.Contains(t))) { VisitFragmentDefinition(fragment, path); } @@ -94,20 +97,17 @@ public void VisitDocument(DocumentNode document) ImmutableStack newpath = path.Push(selectionSet); foreach (ISelectionNode selection in selectionSet.Selections) { - if (selection is FieldNode field) { VisitField(field, type, newpath); } - - if (selection is FragmentSpreadNode fragmentSpread) + else if (selection is FragmentSpreadNode fragmentSpread) { VisitFragmentSpread(fragmentSpread, type, newpath); } - - if (selection is InlineFragmentNode inlineFragment) + else if (selection is InlineFragmentNode inlineFragment) { - VisitInlineFragment(inlineFragment, type, newpath); + VisitInlineFragmentInternal(inlineFragment, type, newpath); } } } @@ -121,14 +121,12 @@ public void VisitDocument(DocumentNode document) ImmutableStack newpath = path.Push(field); if (type is IComplexOutputType complexType - && complexType.Fields.ContainsField(field.Name.Value)) + && complexType.Fields.ContainsField(field.Name.Value) + && field.SelectionSet != null) { - if (field.SelectionSet != null) - { - VisitSelectionSet(field.SelectionSet, - complexType.Fields[field.Name.Value].Type.NamedType(), - newpath); - } + VisitSelectionSet(field.SelectionSet, + complexType.Fields[field.Name.Value].Type.NamedType(), + newpath); } VisitDirectives(field.Directives, newpath); @@ -144,9 +142,8 @@ public void VisitDocument(DocumentNode document) if (path.Last() is DocumentNode d) { string fragmentName = fragmentSpread.Name.Value; - if (_visitedFragments.Add(fragmentName) - && _fragments.TryGetValue(fragmentName, - out FragmentDefinitionNode fragment)) + if (_fragments.TryGetValue(fragmentName, + out FragmentDefinitionNode fragment)) { VisitFragmentDefinition(fragment, newpath); } @@ -155,15 +152,34 @@ public void VisitDocument(DocumentNode document) VisitDirectives(fragmentSpread.Directives, newpath); } - protected virtual void VisitInlineFragment( + private void VisitInlineFragmentInternal( InlineFragmentNode inlineFragment, IType type, ImmutableStack path) { - if (inlineFragment.TypeCondition?.Name?.Value != null - && Schema.TryGetType( - inlineFragment.TypeCondition.Name.Value, - out INamedOutputType typeCondition)) + if (inlineFragment.TypeCondition?.Name?.Value == null) + { + VisitInlineFragment(inlineFragment, type, type, path); + } + else if (Schema.TryGetType( + inlineFragment.TypeCondition.Name.Value, + out INamedOutputType typeCondition)) + { + VisitInlineFragment(inlineFragment, type, typeCondition, path); + } + else + { + VisitInlineFragment(inlineFragment, type, null, path); + } + } + + protected virtual void VisitInlineFragment( + InlineFragmentNode inlineFragment, + IType parentType, + IType typeCondition, + ImmutableStack path) + { + if (typeCondition != null) { ImmutableStack newpath = path.Push(inlineFragment); @@ -182,7 +198,8 @@ public void VisitDocument(DocumentNode document) FragmentDefinitionNode fragmentDefinition, ImmutableStack path) { - if (fragmentDefinition.TypeCondition?.Name?.Value != null + if (MarkFragmentVisited(fragmentDefinition) + && fragmentDefinition.TypeCondition?.Name?.Value != null && Schema.TryGetType( fragmentDefinition.TypeCondition.Name.Value, out INamedOutputType typeCondition)) @@ -222,5 +239,38 @@ protected void ClearVisitedFragments() { _visitedFragments.Clear(); } + + protected bool TryGetFragment( + string fragmentName, + out FragmentDefinitionNode fragment) + { + return _fragments.TryGetValue(fragmentName, out fragment); + } + + protected bool ContainsFragment(string fragmentName) + { + return _fragments.ContainsKey(fragmentName); + } + + protected bool IsFragmentVisited(FragmentDefinitionNode fragmentDefinition) + { + if (fragmentDefinition == null) + { + throw new ArgumentNullException(nameof(fragmentDefinition)); + } + + return _visitedFragments.Contains(fragmentDefinition); + } + + protected bool MarkFragmentVisited(FragmentDefinitionNode fragmentDefinition) + { + if (fragmentDefinition == null) + { + throw new ArgumentNullException(nameof(fragmentDefinition)); + } + + _touchedFragments.Add(fragmentDefinition); + return _visitedFragments.Add(fragmentDefinition); + } } } diff --git a/src/Core/Validation/RequiredArgumentVisitor.cs b/src/Core/Validation/RequiredArgumentVisitor.cs index 2c1394ab3f5..9c9e991545b 100644 --- a/src/Core/Validation/RequiredArgumentVisitor.cs +++ b/src/Core/Validation/RequiredArgumentVisitor.cs @@ -9,8 +9,7 @@ namespace HotChocolate.Validation internal sealed class RequiredArgumentVisitor : QueryVisitorErrorBase { - private readonly Dictionary _directives = - new Dictionary(); + private readonly Dictionary _directives; public RequiredArgumentVisitor(ISchema schema) : base(schema) @@ -23,15 +22,14 @@ public RequiredArgumentVisitor(ISchema schema) IType type, ImmutableStack path) { - if (type is IComplexOutputType complexType) + if (type is IComplexOutputType complexType + && complexType.Fields.ContainsField(field.Name.Value)) { - if (complexType.Fields.ContainsField(field.Name.Value)) - { - ValidateRequiredArguments(field, field.Arguments, - complexType.Fields[field.Name.Value].Arguments); - } + ValidateRequiredArguments(field, field.Arguments, + complexType.Fields[field.Name.Value].Arguments); } + base.VisitField(field, type, path); } diff --git a/src/Core/Validation/ValuesOfCorrectTypeRule.cs b/src/Core/Validation/ValuesOfCorrectTypeRule.cs new file mode 100644 index 00000000000..766b1ad0d3a --- /dev/null +++ b/src/Core/Validation/ValuesOfCorrectTypeRule.cs @@ -0,0 +1,18 @@ +namespace HotChocolate.Validation +{ + /// + /// Literal values must be compatible with the type expected in the position + /// they are found as per the coercion rules defined in the Type System + /// chapter. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Values-of-Correct-Type + /// + internal sealed class ValuesOfCorrectTypeRule + : QueryVisitorValidationErrorBase + { + protected override QueryVisitorErrorBase CreateVisitor(ISchema schema) + { + return new ValuesOfCorrectTypeVisitor(schema); + } + } +} diff --git a/src/Core/Validation/ValuesOfCorrectTypeVisitor.cs b/src/Core/Validation/ValuesOfCorrectTypeVisitor.cs new file mode 100644 index 00000000000..ac02cbc7d08 --- /dev/null +++ b/src/Core/Validation/ValuesOfCorrectTypeVisitor.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace HotChocolate.Validation +{ + internal sealed class ValuesOfCorrectTypeVisitor + : InputObjectFieldVisitorBase + { + private readonly HashSet _visited = + new HashSet(); + private readonly Dictionary _directives; + + public ValuesOfCorrectTypeVisitor(ISchema schema) + : base(schema) + { + _directives = schema.Directives.ToDictionary(t => t.Name); + } + + protected override void VisitOperationDefinition( + OperationDefinitionNode operation, + ImmutableStack path) + { + foreach (VariableDefinitionNode variableDefinition in + operation.VariableDefinitions) + { + if (!variableDefinition.DefaultValue.IsNull()) + { + IType type = ConvertTypeNodeToType(variableDefinition.Type); + IValueNode defaultValue = variableDefinition.DefaultValue; + + if (type is IInputType inputType + && !IsInstanceOfType(inputType, defaultValue)) + { + Errors.Add(new ValidationError( + "The specified value type of variable " + + $"`{variableDefinition.Variable.Name.Value}` " + + "does not match the variable type.", + variableDefinition)); + } + else if (defaultValue is ObjectValueNode ov + && type is InputObjectType iot) + { + VisitObjectValue(iot, ov); + } + } + } + + base.VisitOperationDefinition(operation, path); + } + + protected override void VisitField( + FieldNode field, + IType type, + ImmutableStack path) + { + if (type is IComplexOutputType ct + && ct.Fields.TryGetField(field.Name.Value, out IOutputField f)) + { + foreach (ArgumentNode argument in field.Arguments) + { + VisitArgument(f.Arguments, argument, path); + } + } + + base.VisitField(field, type, path); + } + + protected override void VisitDirective( + DirectiveNode directive, + ImmutableStack path) + { + if (_directives.TryGetValue( + directive.Name.Value, + out Directive d)) + { + foreach (ArgumentNode argument in directive.Arguments) + { + VisitArgument(d.Arguments, argument, path); + } + } + + base.VisitDirective(directive, path); + } + + protected override void VisitObjectValue( + InputObjectType type, + ObjectValueNode objectValue) + { + if (_visited.Add(objectValue)) + { + foreach (ObjectFieldNode fieldValue in objectValue.Fields) + { + if (type.Fields.TryGetField(fieldValue.Name.Value, + out InputField field)) + { + if (IsInstanceOfType(field.Type, fieldValue.Value)) + { + if (fieldValue.Value is ObjectValueNode ov + && field.Type.NamedType() is InputObjectType it) + { + VisitObjectValue(type, objectValue); + } + } + else + { + Errors.Add(new ValidationError( + "The specified value type of field " + + $"`{fieldValue.Name.Value}` " + + "does not match the field type.", + fieldValue)); + } + } + } + } + } + + private void VisitArgument( + IFieldCollection argumentFields, + ArgumentNode argument, + ImmutableStack path) + { + if (argumentFields.TryGetField(argument.Name.Value, + out IInputField argumentField)) + { + if (!(argument.Value is VariableNode) + && !IsInstanceOfType(argumentField.Type, argument.Value)) + { + Errors.Add(new ValidationError( + "The specified value type of argument " + + $"`{argument.Name.Value}` " + + "does not match the argument type.", + argument)); + } + } + } + + private IType ConvertTypeNodeToType(ITypeNode typeNode) + { + if (typeNode is NonNullTypeNode nntn) + { + return new NonNullType(ConvertTypeNodeToType(nntn.Type)); + } + + if (typeNode is ListTypeNode ltn) + { + return new ListType(ConvertTypeNodeToType(ltn.Type)); + } + + if (typeNode is NamedTypeNode ntn) + { + return Schema.GetType(ntn.Name.Value); + } + + throw new NotSupportedException(); + } + + private bool IsInstanceOfType(IInputType inputType, IValueNode value) + { + IInputType internalType = inputType; + if (inputType.IsNonNullType()) + { + internalType = (IInputType)inputType.InnerType(); + } + return internalType.IsInstanceOfType(value); + } + } +} diff --git a/src/Types/Types/Scalars/BooleanType.cs b/src/Types/Types/Scalars/BooleanType.cs index 7076e883618..f3351cabcab 100644 --- a/src/Types/Types/Scalars/BooleanType.cs +++ b/src/Types/Types/Scalars/BooleanType.cs @@ -7,6 +7,8 @@ namespace HotChocolate.Types /// The Boolean scalar type represents true or false. /// Response formats should use a built‐in boolean type if supported; /// otherwise, they should use their representation of the integers 1 and 0. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Boolean /// public sealed class BooleanType : ScalarType diff --git a/src/Types/Types/Scalars/DateTimeTypeBase.cs b/src/Types/Types/Scalars/DateTimeTypeBase.cs index 9376894340f..bbcdc560369 100644 --- a/src/Types/Types/Scalars/DateTimeTypeBase.cs +++ b/src/Types/Types/Scalars/DateTimeTypeBase.cs @@ -6,7 +6,7 @@ namespace HotChocolate.Types public abstract class DateTimeTypeBase : ScalarType { - public DateTimeTypeBase(string name, string description) + protected DateTimeTypeBase(string name, string description) : base(name, description) { } diff --git a/src/Types/Types/Scalars/FloatType.cs b/src/Types/Types/Scalars/FloatType.cs index c3a44843db3..bf3c44f73fa 100644 --- a/src/Types/Types/Scalars/FloatType.cs +++ b/src/Types/Types/Scalars/FloatType.cs @@ -5,6 +5,14 @@ namespace HotChocolate.Types { + /// + /// The Float scalar type represents signed double‐precision fractional + /// values as specified by IEEE 754. Response formats that support an + /// appropriate double‐precision number type should use that type to + /// represent this scalar. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Float + /// public sealed class FloatType : NumberType { @@ -13,6 +21,39 @@ public FloatType() { } + public override bool IsInstanceOfType(IValueNode literal) + { + if (literal == null) + { + throw new ArgumentNullException(nameof(literal)); + } + + // Input coercion rules specify that float values can be coerced + // from IntValueNode and FloatValueNode: + // http://facebook.github.io/graphql/June2018/#sec-Float + return base.IsInstanceOfType(literal) || literal is IntValueNode; + } + + public override object ParseLiteral(IValueNode literal) + { + if (literal == null) + { + throw new ArgumentNullException(nameof(literal)); + } + + // Input coercion rules specify that float values can be coerced + // from IntValueNode and FloatValueNode: + // http://facebook.github.io/graphql/June2018/#sec-Float + if (literal is IntValueNode node) + { + return double.Parse(node.Value, + NumberStyles.Float, + CultureInfo.InvariantCulture); + } + + return base.ParseLiteral(literal); + } + protected override double OnParseLiteral(FloatValueNode node) => double.Parse(node.Value, NumberStyles.Float, CultureInfo.InvariantCulture); diff --git a/src/Types/Types/Scalars/IdType.cs b/src/Types/Types/Scalars/IdType.cs index f52e5f7ba94..e1a2470ac25 100644 --- a/src/Types/Types/Scalars/IdType.cs +++ b/src/Types/Types/Scalars/IdType.cs @@ -3,6 +3,15 @@ namespace HotChocolate.Types { + /// + /// The ID scalar type represents a unique identifier, often used to refetch + /// an object or as the key for a cache. The ID type is serialized in the + /// same way as a String; however, it is not intended to be human‐readable. + /// + /// While it is often numeric, it should always serialize as a String. + /// + /// http://facebook.github.io/graphql/June2018/#sec-ID + /// public sealed class IdType : StringTypeBase { diff --git a/src/Types/Types/Scalars/IntType.cs b/src/Types/Types/Scalars/IntType.cs index 0daf28cf6fc..071ebc556b6 100644 --- a/src/Types/Types/Scalars/IntType.cs +++ b/src/Types/Types/Scalars/IntType.cs @@ -3,6 +3,13 @@ namespace HotChocolate.Types { + /// + /// The Int scalar type represents a signed 32‐bit numeric non‐fractional + /// value. Response formats that support a 32‐bit integer or a number type + /// should use that type to represent this scalar. + /// + /// http://facebook.github.io/graphql/June2018/#sec-Int + /// public sealed class IntType : NumberType { diff --git a/src/Types/Types/Scalars/StringType.cs b/src/Types/Types/Scalars/StringType.cs index 69e371c234d..013fbf544d6 100644 --- a/src/Types/Types/Scalars/StringType.cs +++ b/src/Types/Types/Scalars/StringType.cs @@ -5,11 +5,13 @@ namespace HotChocolate.Types { /// /// The String scalar type represents textual data, represented as - /// UTF‐8 character sequences. The String type is most often used by GraphQL - /// to represent free‐form human‐readable text. + /// UTF‐8 character sequences. The String type is most often used + /// by GraphQL to represent free‐form human‐readable text. /// /// All response formats must support string representations, /// and that representation must be used here. + /// + /// http://facebook.github.io/graphql/June2018/#sec-String /// public sealed class StringType : StringTypeBase diff --git a/src/Types/Types/TypeInitializationContext.cs b/src/Types/Types/TypeInitializationContext.cs index 26c10c3753f..d54104d72c0 100644 --- a/src/Types/Types/TypeInitializationContext.cs +++ b/src/Types/Types/TypeInitializationContext.cs @@ -158,26 +158,28 @@ public bool TryGetNativeType(INamedType namedType, out Type nativeType) public bool TryGetProperty(INamedType namedType, string fieldName, out T member) where T : MemberInfo { - if (namedType is ObjectType) + if (namedType is ObjectType + && _schemaContext.Types.TryGetTypeBinding(namedType, + out ObjectTypeBinding binding) + && binding.Fields.TryGetValue(fieldName, + out FieldBinding fieldBinding) + && fieldBinding.Member is T m) { - if (_schemaContext.Types.TryGetTypeBinding(namedType, out ObjectTypeBinding binding) - && binding.Fields.TryGetValue(fieldName, out FieldBinding fieldBinding) - && fieldBinding.Member is T m) - { - member = m; - return true; - } + member = m; + return true; } - if (namedType is InputObjectType) + + if (namedType is InputObjectType + && _schemaContext.Types.TryGetTypeBinding(namedType, + out InputObjectTypeBinding inputBinding) + && inputBinding.Fields.TryGetValue(fieldName, + out InputFieldBinding inputFieldBinding) + && inputFieldBinding.Property is T p) { - if (_schemaContext.Types.TryGetTypeBinding(namedType, out InputObjectTypeBinding binding) - && binding.Fields.TryGetValue(fieldName, out InputFieldBinding fieldBinding) - && fieldBinding.Property is T p) - { - member = p; - return true; - } + member = p; + return true; + } member = null;