Skip to content

Commit d942259

Browse files
authored
Ensures that FieldResult<T> has errors declared. (#7612)
1 parent 03463b0 commit d942259

8 files changed

+197
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using HotChocolate.Types;
2+
using HotChocolate.Types.Descriptors;
3+
using HotChocolate.Utilities;
4+
5+
namespace HotChocolate.Configuration.Validation;
6+
7+
internal sealed class EnsureFieldResultsDeclareErrorsRule : ISchemaValidationRule
8+
{
9+
private const string _errorKey = "HotChocolate.Types.Errors.ErrorDefinitions";
10+
11+
public void Validate(
12+
IDescriptorContext context,
13+
ISchema schema,
14+
ICollection<ISchemaError> errors)
15+
{
16+
var mutationType = schema.MutationType;
17+
18+
foreach (var objectType in schema.Types.OfType<ObjectType>())
19+
{
20+
if (ReferenceEquals(objectType, mutationType))
21+
{
22+
continue;
23+
}
24+
25+
foreach (var field in objectType.Fields.AsSpan())
26+
{
27+
var member = field.ResolverMember ?? field.Member;
28+
if (member is not null)
29+
{
30+
var returnType = member.GetReturnType();
31+
if (returnType is not null
32+
&& returnType.IsGenericType
33+
&& returnType.GenericTypeArguments.Length == 1)
34+
{
35+
var typeDefinition = returnType.GetGenericTypeDefinition();
36+
37+
if (typeDefinition == typeof(FieldResult<>))
38+
{
39+
EnsureErrorsAreDefined(field, errors);
40+
}
41+
else if (typeDefinition == typeof(ValueTask<>) || typeDefinition == typeof(Task<>))
42+
{
43+
var type = returnType.GenericTypeArguments[0];
44+
if (type.IsGenericType
45+
&& type.GenericTypeArguments.Length == 1
46+
&& type.GetGenericTypeDefinition() == typeof(FieldResult<>))
47+
{
48+
EnsureErrorsAreDefined(field, errors);
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
57+
private static void EnsureErrorsAreDefined(
58+
ObjectField field,
59+
ICollection<ISchemaError> errors)
60+
{
61+
if (!field.ContextData.ContainsKey(_errorKey))
62+
{
63+
errors.Add(
64+
SchemaErrorBuilder.New()
65+
.SetMessage(
66+
"The field `{0}` must declare errors to use a FieldResult<T>.",
67+
field.Coordinate)
68+
.SetTypeSystemObject(field.DeclaringType)
69+
.Build());
70+
}
71+
}
72+
}

src/HotChocolate/Core/src/Types/Configuration/Validation/SchemaValidator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal static class SchemaValidator
1212
new DirectiveValidationRule(),
1313
new InterfaceHasAtLeastOneImplementationRule(),
1414
new IsSelectedPatternValidation(),
15+
new EnsureFieldResultsDeclareErrorsRule()
1516
];
1617

1718
public static IReadOnlyList<ISchemaError> Validate(

src/HotChocolate/Core/test/Types.Queries.Tests/CodeFirstSchemaTests.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,78 @@ async Task Error() =>
354354
exception.Errors[0].Message.MatchSnapshot();
355355
}
356356

357+
[Fact]
358+
public async Task Throw_SchemaError_When_FieldResult_Has_No_Errors()
359+
{
360+
async Task Error()
361+
=> await new ServiceCollection()
362+
.AddGraphQL()
363+
.AddQueryConventions()
364+
.AddQueryType<InvalidQuery>()
365+
.BuildSchemaAsync();
366+
367+
var exception = await Assert.ThrowsAsync<SchemaException>(Error);
368+
Assert.Single(exception.Errors);
369+
exception.Errors[0].Message.MatchSnapshot();
370+
}
371+
372+
[Fact]
373+
public async Task Throw_SchemaError_When_FieldResult_Has_No_Errors_1()
374+
{
375+
async Task Error()
376+
=> await new ServiceCollection()
377+
.AddGraphQL()
378+
.AddQueryType<InvalidQuery>()
379+
.BuildSchemaAsync();
380+
381+
var exception = await Assert.ThrowsAsync<SchemaException>(Error);
382+
Assert.Single(exception.Errors);
383+
exception.Errors[0].Message.MatchSnapshot();
384+
}
385+
386+
[Fact]
387+
public async Task Throw_SchemaError_When_FieldResult_Has_No_Errors_2()
388+
{
389+
async Task Error()
390+
=> await new ServiceCollection()
391+
.AddGraphQL()
392+
.AddQueryConventions()
393+
.AddQueryType<InvalidQueryTask>()
394+
.BuildSchemaAsync();
395+
396+
var exception = await Assert.ThrowsAsync<SchemaException>(Error);
397+
Assert.Single(exception.Errors);
398+
exception.Errors[0].Message.MatchSnapshot();
399+
}
400+
401+
[Fact]
402+
public async Task Throw_SchemaError_When_FieldResult_Has_No_Errors_3()
403+
{
404+
async Task Error()
405+
=> await new ServiceCollection()
406+
.AddGraphQL()
407+
.AddQueryConventions()
408+
.AddQueryType<InvalidQueryValueTask>()
409+
.BuildSchemaAsync();
410+
411+
var exception = await Assert.ThrowsAsync<SchemaException>(Error);
412+
Assert.Single(exception.Errors);
413+
exception.Errors[0].Message.MatchSnapshot();
414+
}
415+
416+
[Fact]
417+
public async Task FieldResult_With_Errors_Are_Valid()
418+
{
419+
var schema =
420+
await new ServiceCollection()
421+
.AddGraphQL()
422+
.AddQueryConventions()
423+
.AddQueryType<ValidQueryValueTask>()
424+
.BuildSchemaAsync();
425+
426+
schema.MatchSnapshot();
427+
}
428+
357429
public class QueryWithFieldResultType : ObjectType
358430
{
359431
protected override void Configure(IObjectTypeDescriptor descriptor)
@@ -507,4 +579,30 @@ public sealed record AddressNotFound(string Id, string Message);
507579
public sealed class UserNotFoundException : Exception;
508580

509581
public sealed class InvalidUserIdException : Exception;
582+
583+
public class InvalidQuery
584+
{
585+
public FieldResult<Foo> Foo() => default!;
586+
}
587+
588+
public class InvalidQueryTask
589+
{
590+
public Task<FieldResult<Foo>> Foo() => default!;
591+
}
592+
593+
public class InvalidQueryValueTask
594+
{
595+
public Task<FieldResult<Foo>> Foo() => default!;
596+
}
597+
598+
public class ValidQueryValueTask
599+
{
600+
[Error<ArgumentException>]
601+
public Task<FieldResult<Foo>> Foo() => default!;
602+
}
603+
604+
public class Foo
605+
{
606+
public string Bar => default!;
607+
}
510608
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
schema {
2+
query: ValidQueryValueTask
3+
}
4+
5+
interface Error {
6+
message: String!
7+
}
8+
9+
type ArgumentError implements Error {
10+
message: String!
11+
paramName: String
12+
}
13+
14+
type Foo {
15+
bar: String!
16+
}
17+
18+
type ValidQueryValueTask {
19+
foo: FooResult!
20+
}
21+
22+
union FooResult = Foo | ArgumentError
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The field `InvalidQuery.foo` must declare errors to use a FieldResult<T>.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The field `InvalidQuery.foo` must declare errors to use a FieldResult<T>.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The field `InvalidQueryTask.foo` must declare errors to use a FieldResult<T>.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The field `InvalidQueryValueTask.foo` must declare errors to use a FieldResult<T>.

0 commit comments

Comments
 (0)