Skip to content

Commit b6797c0

Browse files
authored
[Fusion] Added post-merge validation rule "IsInvalidFieldRule" (#8217)
1 parent b200d4f commit b6797c0

File tree

8 files changed

+204
-0
lines changed

8 files changed

+204
-0
lines changed

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/DirectivesProviderExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ public static bool HasInaccessibleDirective(this IDirectivesProvider type)
5252
return type.Directives.ContainsName(DirectiveNames.Inaccessible);
5353
}
5454

55+
public static bool HasIsDirective(this IDirectivesProvider type)
56+
{
57+
return type.Directives.ContainsName(DirectiveNames.Is);
58+
}
59+
5560
public static bool HasLookupDirective(this IDirectivesProvider type)
5661
{
5762
return type.Directives.ContainsName(DirectiveNames.Lookup);

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static class LogEntryCodes
2222
public const string InterfaceFieldNoImplementation = "INTERFACE_FIELD_NO_IMPLEMENTATION";
2323
public const string InvalidGraphQL = "INVALID_GRAPHQL";
2424
public const string InvalidShareableUsage = "INVALID_SHAREABLE_USAGE";
25+
public const string IsInvalidField = "IS_INVALID_FIELD";
2526
public const string IsInvalidFieldType = "IS_INVALID_FIELD_TYPE";
2627
public const string IsInvalidSyntax = "IS_INVALID_SYNTAX";
2728
public const string IsInvalidUsage = "IS_INVALID_USAGE";

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,26 @@ public static LogEntry InvalidShareableUsage(
422422
schema);
423423
}
424424

425+
public static LogEntry IsInvalidField(
426+
Directive isDirective,
427+
string argumentName,
428+
string fieldName,
429+
string typeName,
430+
MutableSchemaDefinition sourceSchema,
431+
ImmutableArray<string> errors)
432+
{
433+
var coordinate = new SchemaCoordinate(typeName, fieldName, argumentName);
434+
435+
return new LogEntry(
436+
string.Format(LogEntryHelper_IsInvalidField, coordinate, sourceSchema.Name),
437+
LogEntryCodes.IsInvalidField,
438+
LogSeverity.Error,
439+
coordinate,
440+
isDirective,
441+
sourceSchema,
442+
errors);
443+
}
444+
425445
public static LogEntry IsInvalidFieldType(
426446
Directive isDirective,
427447
string argumentName,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using HotChocolate.Fusion.Events;
2+
using HotChocolate.Fusion.Events.Contracts;
3+
using HotChocolate.Fusion.Extensions;
4+
using HotChocolate.Fusion.Info;
5+
using HotChocolate.Fusion.Language;
6+
using HotChocolate.Fusion.Validators;
7+
using HotChocolate.Types;
8+
using HotChocolate.Types.Mutable;
9+
using static HotChocolate.Fusion.Logging.LogEntryHelper;
10+
using static HotChocolate.Fusion.WellKnownArgumentNames;
11+
using static HotChocolate.Fusion.WellKnownDirectiveNames;
12+
13+
namespace HotChocolate.Fusion.PostMergeValidationRules;
14+
15+
/// <summary>
16+
/// Even if the field selection map for <c>@is(field: "…")</c> is syntactically valid, its contents
17+
/// must also be valid within the composed schema. Fields must exist on the parent type for them to
18+
/// be referenced by <c>@is</c>. In addition, fields referencing unknown fields break the valid
19+
/// usage of <c>@is</c>, leading to an <c>IS_INVALID_FIELD</c> error.
20+
/// </summary>
21+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Is-Invalid-Field">
22+
/// Specification
23+
/// </seealso>
24+
internal sealed class IsInvalidFieldRule : IEventHandler<SchemaEvent>
25+
{
26+
public void Handle(SchemaEvent @event, CompositionContext context)
27+
{
28+
var schema = @event.Schema;
29+
30+
var sourceArgumentGroup = context.SchemaDefinitions
31+
.SelectMany(s => s.Types.OfType<MutableObjectTypeDefinition>(), (s, o) => (s, o))
32+
.SelectMany(x => x.o.Fields.AsEnumerable(), (x, f) => (x.s, x.o, f))
33+
.SelectMany(
34+
x => x.f.Arguments.AsEnumerable().Where(a => a.HasIsDirective()),
35+
(x, a) => new FieldArgumentInfo(a, x.f, x.o, x.s));
36+
37+
var validator = new FieldSelectionMapValidator(schema);
38+
39+
foreach (var (sourceArgument, sourceField, sourceType, sourceSchema) in sourceArgumentGroup)
40+
{
41+
var isDirective = sourceArgument.Directives[Is].First();
42+
var fieldArgumentValue = (string)isDirective.Arguments[Field].Value!;
43+
var fieldSelectionMapParser = new FieldSelectionMapParser(fieldArgumentValue);
44+
var fieldSelectionMap = fieldSelectionMapParser.Parse();
45+
var inputType = schema.Types[sourceArgument.Type.AsTypeDefinition().Name];
46+
var outputType = schema.Types[sourceField.Type.AsTypeDefinition().Name];
47+
48+
var errors = validator.Validate(fieldSelectionMap, inputType, outputType);
49+
50+
if (errors.Any())
51+
{
52+
context.Log.Write(
53+
IsInvalidField(
54+
isDirective,
55+
sourceArgument.Name,
56+
sourceField.Name,
57+
sourceType.Name,
58+
sourceSchema,
59+
errors));
60+
}
61+
}
62+
}
63+
}

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@
207207
<data name="LogEntryHelper_InvalidShareableUsage" xml:space="preserve">
208208
<value>The interface field '{0}' in schema '{1}' must not be marked as shareable.</value>
209209
</data>
210+
<data name="LogEntryHelper_IsInvalidField" xml:space="preserve">
211+
<value>The @is directive on argument '{0}' in schema '{1}' specifies an invalid field selection against the composed schema.</value>
212+
</data>
210213
<data name="LogEntryHelper_IsInvalidFieldType" xml:space="preserve">
211214
<value>The @is directive on argument '{0}' in schema '{1}' must specify a string value for the 'field' argument.</value>
212215
</data>

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ public CompositionResult<MutableSchemaDefinition> Compose()
132132
new EmptyMergedUnionTypeRule(),
133133
new EnumTypeDefaultValueInaccessibleRule(),
134134
new InterfaceFieldNoImplementationRule(),
135+
new IsInvalidFieldRule(),
135136
new NonNullInputFieldIsInaccessibleRule(),
136137
new NoQueriesRule(),
137138
new RequireInvalidFieldsRule()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Logging;
3+
using static HotChocolate.Fusion.CompositionTestHelper;
4+
5+
namespace HotChocolate.Fusion.PostMergeValidationRules;
6+
7+
public sealed class IsInvalidFieldRuleTests
8+
{
9+
private static readonly object s_rule = new IsInvalidFieldRule();
10+
private static readonly ImmutableArray<object> s_rules = [s_rule];
11+
private readonly CompositionLog _log = new();
12+
13+
[Theory]
14+
[MemberData(nameof(ValidExamplesData))]
15+
public void Examples_Valid(string[] sdl)
16+
{
17+
// arrange
18+
var schemas = CreateSchemaDefinitions(sdl);
19+
var merger = new SourceSchemaMerger(schemas);
20+
var mergeResult = merger.Merge();
21+
var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log);
22+
23+
// act
24+
var result = validator.Validate();
25+
26+
// assert
27+
Assert.True(result.IsSuccess);
28+
Assert.True(_log.IsEmpty);
29+
}
30+
31+
[Theory]
32+
[MemberData(nameof(InvalidExamplesData))]
33+
public void Examples_Invalid(string[] sdl, string[] errorMessages)
34+
{
35+
// arrange
36+
var schemas = CreateSchemaDefinitions(sdl);
37+
var merger = new SourceSchemaMerger(schemas);
38+
var mergeResult = merger.Merge();
39+
var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log);
40+
41+
// act
42+
var result = validator.Validate();
43+
44+
// assert
45+
Assert.True(result.IsFailure);
46+
Assert.Equal(errorMessages, _log.Select(e => e.Message).ToArray());
47+
Assert.True(_log.All(e => e.Code == "IS_INVALID_FIELD"));
48+
Assert.True(_log.All(e => e.Severity == LogSeverity.Error));
49+
}
50+
51+
public static TheoryData<string[]> ValidExamplesData()
52+
{
53+
return new TheoryData<string[]>
54+
{
55+
// In the following example, the @is directive’s "field" argument is a valid field
56+
// selection map and satisfies the rule.
57+
{
58+
[
59+
"""
60+
# Schema A
61+
type Query {
62+
personById(id: ID! @is(field: "id")): Person @lookup
63+
}
64+
65+
type Person {
66+
id: ID!
67+
name: String
68+
}
69+
"""
70+
]
71+
}
72+
};
73+
}
74+
75+
public static TheoryData<string[], string[]> InvalidExamplesData()
76+
{
77+
return new TheoryData<string[], string[]>
78+
{
79+
// In this example, the @is directive references a field ("unknownField") that does
80+
// not exist on the return type ("Person"), causing an IS_INVALID_FIELD error.
81+
{
82+
[
83+
"""
84+
# Schema A
85+
type Query {
86+
personById(id: ID! @is(field: "unknownField")): Person @lookup
87+
}
88+
89+
type Person {
90+
id: ID!
91+
name: String
92+
}
93+
"""
94+
],
95+
[
96+
"The @is directive on argument 'Query.personById(id:)' in schema 'A' " +
97+
"specifies an invalid field selection against the composed schema."
98+
]
99+
}
100+
};
101+
}
102+
}

0 commit comments

Comments
 (0)