Skip to content

Commit

Permalink
Merge pull request #27 from graphql-dotnet/validation-and-error-handling
Browse files Browse the repository at this point in the history
Validation and detailed error information
  • Loading branch information
tlil authored Mar 1, 2017
2 parents 41994f6 + 6c46eef commit 540a4ab
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 33 deletions.
2 changes: 2 additions & 0 deletions src/GraphQL.Conventions/Adapters/Engine/GraphQLEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,12 @@ internal async Task<ExecutionResult> Execute(
{
var exception = new FieldResolutionException(executionError);
var error = new ExecutionError(exception.Message, exception);
error.SetCode(exception.InnerException ?? exception);
foreach (var location in executionError.Locations ?? new ErrorLocation[0])
{
error.AddLocation(location.Line, location.Column);
}
error.Path.AddRange(executionError.Path);
errors.Add(error);
}
result.Errors = errors;
Expand Down
6 changes: 3 additions & 3 deletions src/GraphQL.Conventions/CommonAssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
[assembly: AssemblyProduct("GraphQL.Conventions")]
[assembly: AssemblyCopyright("Copyright 2016-2017 Tommy Lillehagen. All rights reserved.")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyVersion("1.1.6.0")]
[assembly: AssemblyFileVersion("1.1.6.0")]
[assembly: AssemblyInformationalVersion("1.1.6.0")]
[assembly: AssemblyVersion("1.1.7.0")]
[assembly: AssemblyFileVersion("1.1.7.0")]
[assembly: AssemblyInformationalVersion("1.1.7.0")]
[assembly: CLSCompliant(false)]

[assembly: InternalsVisibleTo("GraphQL.Conventions.Tests")]
4 changes: 4 additions & 0 deletions src/GraphQL.Conventions/Execution/FieldResolutionException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public FieldResolutionException(Exception exception)
private static string DeriveMessage(Exception exception)
{
var innerException = ExtractException(exception.InnerException ?? exception);
if (exception.Message.StartsWith("Error trying to resolve "))
{
return innerException?.Message ?? exception.Message;
}
return innerException != null && exception.Message != innerException.Message
? $"{exception.Message.TrimEnd('.')}. {innerException.Message}"
: exception.Message;
Expand Down
3 changes: 0 additions & 3 deletions src/GraphQL.Conventions/Web/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ public static class Extensions
public static Request ToGraphQLRequest(this Stream stream) =>
Request.New(stream);

public static Response ToResponse(this ExecutionResult result, Request request, GraphQLEngine engine) =>
new Response(request, result, engine.SerializeResult(result));

public static string IdentifierForTypeOrNull<T>(this Id id) =>
id.IsIdentifierForType<T>() ? id.IdentifierForType<T>() : null;
}
Expand Down
42 changes: 23 additions & 19 deletions src/GraphQL.Conventions/Web/RequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class RequestHandlerBuilder : IDependencyInjector

ResolveTypeDelegate _resolveTypeDelegate;

bool _useValidation = true;

internal RequestHandlerBuilder()
{
_dependencyInjector = this;
Expand Down Expand Up @@ -91,13 +93,20 @@ public RequestHandlerBuilder TreatAsWarning<TException>()
return this;
}

public RequestHandlerBuilder WithoutValidation()
{
_useValidation = false;
return this;
}

public IRequestHandler Generate()
{
return new RequestHandlerImpl(
_dependencyInjector,
_schemaTypes,
_assemblyTypes,
_exceptionsTreatedAsWarnings);
_exceptionsTreatedAsWarnings,
_useValidation);
}

public object Resolve(TypeInfo typeInfo)
Expand All @@ -114,15 +123,19 @@ class RequestHandlerImpl : IRequestHandler

readonly List<Type> _exceptionsTreatedAsWarnings = new List<Type>();

readonly bool _useValidation;

internal RequestHandlerImpl(
IDependencyInjector dependencyInjector,
IEnumerable<Type> schemaTypes,
IEnumerable<Type> assemblyTypes,
IEnumerable<Type> exceptionsTreatedAsWarning)
IEnumerable<Type> exceptionsTreatedAsWarning,
bool useValidation)
{
_dependencyInjector = dependencyInjector;
_exceptionsTreatedAsWarnings.AddRange(exceptionsTreatedAsWarning);
_engine.WithAttributesFromAssemblies(assemblyTypes);
_exceptionsTreatedAsWarnings.AddRange(exceptionsTreatedAsWarning);
_useValidation = useValidation;
_engine.BuildSchema(schemaTypes.ToArray());
}

Expand All @@ -135,25 +148,16 @@ public async Task<Response> ProcessRequest(Request request, IUserContext userCon
.WithOperationName(request.OperationName)
.WithDependencyInjector(_dependencyInjector)
.WithUserContext(userContext)
.EnableValidation(_useValidation)
.Execute()
.ConfigureAwait(false);
return new Response(request, result, _engine.SerializeResult(result));
}

var response = result.ToResponse(request, _engine);
var errors = result.Errors?.Where(e => !string.IsNullOrWhiteSpace(e?.Message));

foreach (var error in errors ?? new List<ExecutionError>())
{
if (_exceptionsTreatedAsWarnings.Contains(error.InnerException.GetType()))
{
response.Warnings.Add(error);
}
else
{
response.Errors.Add(error);
}
}

return response;
public Response Validate(Request request)
{
var result = _engine.Validate(request.QueryString);
return new Response(request, result);
}

public string DescribeSchema(bool returnJson = false)
Expand Down
44 changes: 40 additions & 4 deletions src/GraphQL.Conventions/Web/Response.cs
Original file line number Diff line number Diff line change
@@ -1,29 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace GraphQL.Conventions.Web
{
public class Response
{
public Response(Request request, ExecutionResult result, string serializedResult)
public Response(
Request request,
ExecutionResult result,
string serializedResult,
List<Type> exceptionsTreatedAsWarnings = null)
{
Request = request;
Result = result;
ExecutionResult = result;
Body = serializedResult;
PopulateErrorsAndWarnings(exceptionsTreatedAsWarnings);
}

public Response(
Request request,
Validation.IValidationResult result,
List<Type> exceptionsTreatedAsWarnings = null)
{
Request = request;
ValidationResult = result;
PopulateErrorsAndWarnings(exceptionsTreatedAsWarnings);
}

private void PopulateErrorsAndWarnings(List<Type> exceptionsTreatedAsWarnings)
{
var errors = Errors?.Where(e => !string.IsNullOrWhiteSpace(e?.Message));
foreach (var error in errors ?? new List<ExecutionError>())
{
if (exceptionsTreatedAsWarnings.Contains(error.InnerException.GetType()))
{
Warnings.Add(error);
}
else
{
Errors.Add(error);
}
}
}

public Request Request { get; private set; }

public string QueryId => Request?.QueryId;

public ExecutionResult Result { get; private set; }
public ExecutionResult ExecutionResult { get; private set; }

public Validation.IValidationResult ValidationResult { get; private set; }

public string Body { get; }

public bool HasData => Result.Data != null;
public bool HasData => ExecutionResult?.Data != null;

public bool HasErrors => (Errors?.Any() ?? false) || (Warnings?.Any() ?? false);

public bool IsValid => ValidationResult != null ? ValidationResult.IsValid : !HasErrors;

public IList<ExecutionError> Errors { get; } = new List<ExecutionError>();

public IList<ExecutionError> Warnings { get; } = new List<ExecutionError>();
Expand Down
2 changes: 1 addition & 1 deletion src/GraphQL.Conventions/project.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.1.6-*",
"version": "1.1.7-*",
"description": "GraphQL Conventions for .NET",
"authors": [
"Tommy Lillehagen"
Expand Down
149 changes: 149 additions & 0 deletions test/Tests/Adapters/Engine/ErrorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GraphQL.Conventions.Tests.Templates;
using GraphQL.Conventions.Tests.Templates.Extensions;

namespace GraphQL.Conventions.Tests.Adapters.Engine
{
public class ErrorTests : TestBase
{
[Test]
public async void Will_Provide_Path_And_Code_For_Errors_On_Fields_With_Operation_Name()
{
var engine = GraphQLEngine.New<Query>();
var result = await engine
.NewExecutor()
.WithQueryString("query Blah { getObject { field { test } } }")
.Execute();

result.Errors.ShouldNotBeNull();
result.Errors.Count.ShouldEqual(1);
var error = result.Errors.First();
error.Message.ShouldEqual("Test error.");
error.Code.ShouldEqual("ARGUMENT");
error.Path.Count.ShouldEqual(4);
error.Path[0].ShouldEqual("Blah");
error.Path[1].ShouldEqual("getObject");
error.Path[2].ShouldEqual("field");
error.Path[3].ShouldEqual("test");
}

[Test]
public async void Will_Provide_Path_And_Code_For_Errors_On_Fields_Without_Operation_Name()
{
var engine = GraphQLEngine.New<Query>();
var result = await engine
.NewExecutor()
.WithQueryString("{ getObject { field { test } } }")
.Execute();

result.Errors.ShouldNotBeNull();
result.Errors.Count.ShouldEqual(1);
var error = result.Errors.First();
error.Message.ShouldEqual("Test error.");
error.Code.ShouldEqual("ARGUMENT");
error.Path.Count.ShouldEqual(4);
error.Path[0].ShouldEqual(null);
error.Path[1].ShouldEqual("getObject");
error.Path[2].ShouldEqual("field");
error.Path[3].ShouldEqual("test");
}

[Test]
public async void Will_Provide_Path_And_Code_For_Errors_On_Fields_With_Aliases()
{
var engine = GraphQLEngine.New<Query>();
var result = await engine
.NewExecutor()
.WithQueryString("{ yo: getObject { foo: field { bar: test } } }")
.Execute();

result.Errors.ShouldNotBeNull();
result.Errors.Count.ShouldEqual(1);
var error = result.Errors.First();
error.Message.ShouldEqual("Test error.");
error.Code.ShouldEqual("ARGUMENT");
error.Path.Count.ShouldEqual(4);
error.Path[0].ShouldEqual(null);
error.Path[1].ShouldEqual("yo");
error.Path[2].ShouldEqual("foo");
error.Path[3].ShouldEqual("bar");
}

[Test]
public async void Will_Provide_Path_And_Code_For_Errors_In_Array_Fields()
{
var engine = GraphQLEngine.New<Query>();
var result = await engine
.NewExecutor()
.WithQueryString("query Blah { getObject { arrayField { test } } }")
.Execute();

result.Data.ShouldHaveFieldWithValue("getObject", "arrayField", 0, "test", "some value");
result.Data.ShouldHaveFieldWithValue("getObject", "arrayField", 1, "test", null);
result.Data.ShouldHaveFieldWithValue("getObject", "arrayField", 2, "test", "some value");
result.Data.ShouldHaveFieldWithValue("getObject", "arrayField", 3, "test", null);

result.Errors.ShouldNotBeNull();
result.Errors.Count.ShouldEqual(2);

var error = result.Errors.ElementAt(0);
error.Message.ShouldEqual("Test error.");
error.Code.ShouldEqual("ARGUMENT");
error.Path.Count.ShouldEqual(5);
error.Path[0].ShouldEqual("Blah");
error.Path[1].ShouldEqual("getObject");
error.Path[2].ShouldEqual("arrayField");
error.Path[3].ShouldEqual("1");
error.Path[4].ShouldEqual("test");

error = result.Errors.ElementAt(1);
error.Message.ShouldEqual("Test error.");
error.Code.ShouldEqual("ARGUMENT");
error.Path.Count.ShouldEqual(5);
error.Path[0].ShouldEqual("Blah");
error.Path[1].ShouldEqual("getObject");
error.Path[2].ShouldEqual("arrayField");
error.Path[3].ShouldEqual("3");
error.Path[4].ShouldEqual("test");
}

class Query
{
public Object1 GetObject() => new Object1();
}

class Object1
{
public Object2 Field => new Object2();

public List<Object2> ArrayField => new List<Object2>
{
new Object2(false),
new Object2(true),
new Object2(false),
new Object2(true),
};
}

class Object2
{
private readonly bool _throwError;

public Object2(bool throwError = true)
{
_throwError = throwError;
}

public string Test()
{
if (_throwError)
{
throw new ArgumentException("Test error.");
}
return "some value";
}
}
}
}
3 changes: 3 additions & 0 deletions test/Tests/Adapters/Engine/GraphQLEngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ enum Enum1 {
type QueryWithEnums {
field1: Enum1!
field2: RenamedEnum
field3(arg: RenamedEnum! = SOME_VALUE2): RenamedEnum!
}
enum RenamedEnum {
SOME_VALUE1
Expand Down Expand Up @@ -279,6 +280,8 @@ class QueryWithEnums
public Enum1 Field1 => Enum1.Option1;

public Enum2? Field2 => Enum2.SomeValue1;

public Enum2 Field3(Enum2 arg = Enum2.SomeValue2) => arg;
}

enum Enum1
Expand Down
2 changes: 0 additions & 2 deletions test/Tests/Adapters/FieldResolutionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ public async void Throws_Exception_With_Correct_StackTrace_On_Error()
var result = await ExecuteQuery("{ errorField }");
result.ShouldHaveErrors(1);
var error = result.Errors.First();
error.Message.ShouldContain("Error trying to resolve errorField");
error.InnerException.ToString().ShouldContainWhenReformatted("System.NotImplementedException: The method or operation is not implemented");
error.InnerException.ToString().ShouldContainWhenReformatted("at GraphQL.Conventions.Tests.Adapters.FieldResolutionTests.Query.ErrorField()");
}
Expand All @@ -245,7 +244,6 @@ public async void Throws_Exception_With_Correct_StackTrace_On_Task_Error()
var result = await ExecuteQuery("{ errorTaskField }");
result.ShouldHaveErrors(1);
var error = result.Errors.First();
error.Message.ShouldContain("Error trying to resolve errorTaskField");
error.InnerException.ToString().ShouldContainWhenReformatted("System.NotImplementedException: The method or operation is not implemented");
error.InnerException.ToString().ShouldContainWhenReformatted("at GraphQL.Conventions.Tests.Adapters.FieldResolutionTests.Query.ErrorTaskField()");
}
Expand Down

0 comments on commit 540a4ab

Please sign in to comment.