Skip to content

Commit 217b2e0

Browse files
authored
[Fusion] Added post-merge validation rule "EmptyMergedObjectTypeRule" (#7970)
1 parent e9ddd7d commit 217b2e0

File tree

8 files changed

+229
-2
lines changed

8 files changed

+229
-2
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using HotChocolate.Skimmed;
2+
3+
namespace HotChocolate.Fusion.Extensions;
4+
5+
internal static class SchemaDefinitionExtensions
6+
{
7+
public static bool IsRootOperationType(this SchemaDefinition schema, ObjectTypeDefinition type)
8+
{
9+
return
10+
schema.QueryType == type
11+
|| schema.MutationType == type
12+
|| schema.SubscriptionType == type;
13+
}
14+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace HotChocolate.Fusion.Logging;
33
public static class LogEntryCodes
44
{
55
public const string DisallowedInaccessible = "DISALLOWED_INACCESSIBLE";
6+
public const string EmptyMergedObjectType = "EMPTY_MERGED_OBJECT_TYPE";
67
public const string EnumValuesMismatch = "ENUM_VALUES_MISMATCH";
78
public const string ExternalArgumentDefaultMismatch = "EXTERNAL_ARGUMENT_DEFAULT_MISMATCH";
89
public const string ExternalMissingOnBase = "EXTERNAL_MISSING_ON_BASE";

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,19 @@ public static LogEntry DisallowedInaccessibleDirectiveArgument(
100100
schema);
101101
}
102102

103+
public static LogEntry EmptyMergedObjectType(
104+
ObjectTypeDefinition objectType,
105+
SchemaDefinition schema)
106+
{
107+
return new LogEntry(
108+
string.Format(LogEntryHelper_EmptyMergedObjectType, objectType.Name),
109+
LogEntryCodes.EmptyMergedObjectType,
110+
LogSeverity.Error,
111+
new SchemaCoordinate(objectType.Name),
112+
objectType,
113+
schema);
114+
}
115+
103116
public static LogEntry EnumValuesMismatch(
104117
EnumTypeDefinition enumType,
105118
string enumValue,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using HotChocolate.Fusion.Events;
2+
using HotChocolate.Fusion.Events.Contracts;
3+
using HotChocolate.Fusion.Extensions;
4+
using static HotChocolate.Fusion.Logging.LogEntryHelper;
5+
6+
namespace HotChocolate.Fusion.PostMergeValidationRules;
7+
8+
/// <summary>
9+
/// For object types defined across multiple source schemas, the merged object type is the superset
10+
/// of all fields defined in these source schemas. However, any field marked with
11+
/// <c>@inaccessible</c> in any source schema is hidden and not included in the merged object type.
12+
/// An object type with no fields, after considering <c>@inaccessible</c> annotations, is considered
13+
/// empty and invalid.
14+
/// </summary>
15+
/// <seealso href="https://graphql.github.io/composite-schemas-spec/draft/#sec-Empty-Merged-Object-Type">
16+
/// Specification
17+
/// </seealso>
18+
internal sealed class EmptyMergedObjectTypeRule : IEventHandler<ObjectTypeEvent>
19+
{
20+
public void Handle(ObjectTypeEvent @event, CompositionContext context)
21+
{
22+
var (objectType, schema) = @event;
23+
24+
if (schema.IsRootOperationType(objectType) || objectType.HasInaccessibleDirective())
25+
{
26+
return;
27+
}
28+
29+
var accessibleFields = objectType.Fields.Where(f => !f.HasInaccessibleDirective());
30+
31+
if (!accessibleFields.Any())
32+
{
33+
context.Log.Write(EmptyMergedObjectType(objectType, schema));
34+
}
35+
}
36+
}

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

Lines changed: 10 additions & 1 deletion
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
@@ -45,6 +45,9 @@
4545
<data name="LogEntryHelper_DisallowedInaccessibleDirectiveArgument" xml:space="preserve">
4646
<value>The built-in directive argument '{0}' in schema '{1}' is not accessible.</value>
4747
</data>
48+
<data name="LogEntryHelper_EmptyMergedObjectType" xml:space="preserve">
49+
<value>The merged object type '{0}' is empty.</value>
50+
</data>
4851
<data name="LogEntryHelper_EnumValuesMismatch" xml:space="preserve">
4952
<value>The enum type '{0}' in schema '{1}' must define the value '{2}'.</value>
5053
</data>

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Immutable;
22
using HotChocolate.Fusion.Logging.Contracts;
3+
using HotChocolate.Fusion.PostMergeValidationRules;
34
using HotChocolate.Fusion.PreMergeValidationRules;
45
using HotChocolate.Fusion.Results;
56
using HotChocolate.Fusion.SourceSchemaValidationRules;
@@ -117,5 +118,8 @@ public CompositionResult<SchemaDefinition> Compose()
117118
new TypeKindMismatchRule()
118119
];
119120

120-
private static readonly ImmutableArray<object> s_postMergeRules = [];
121+
private static readonly ImmutableArray<object> s_postMergeRules =
122+
[
123+
new EmptyMergedObjectTypeRule()
124+
];
121125
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Logging;
3+
4+
namespace HotChocolate.Fusion.PostMergeValidationRules;
5+
6+
public sealed class EmptyMergedObjectTypeRuleTests : CompositionTestBase
7+
{
8+
private static readonly object s_rule = new EmptyMergedObjectTypeRule();
9+
private static readonly ImmutableArray<object> s_rules = [s_rule];
10+
private readonly CompositionLog _log = new();
11+
12+
[Theory]
13+
[MemberData(nameof(ValidExamplesData))]
14+
public void Examples_Valid(string[] sdl)
15+
{
16+
// arrange
17+
var schemas = CreateSchemaDefinitions(sdl);
18+
var merger = new SourceSchemaMerger(schemas);
19+
var mergeResult = merger.Merge();
20+
var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log);
21+
22+
// act
23+
var result = validator.Validate();
24+
25+
// assert
26+
Assert.True(result.IsSuccess);
27+
Assert.True(_log.IsEmpty);
28+
}
29+
30+
[Theory]
31+
[MemberData(nameof(InvalidExamplesData))]
32+
public void Examples_Invalid(string[] sdl, string[] errorMessages)
33+
{
34+
// arrange
35+
var schemas = CreateSchemaDefinitions(sdl);
36+
var merger = new SourceSchemaMerger(schemas);
37+
var mergeResult = merger.Merge();
38+
var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log);
39+
40+
// act
41+
var result = validator.Validate();
42+
43+
// assert
44+
Assert.True(result.IsFailure);
45+
Assert.Equal(errorMessages, _log.Select(e => e.Message).ToArray());
46+
Assert.True(_log.All(e => e.Code == "EMPTY_MERGED_OBJECT_TYPE"));
47+
Assert.True(_log.All(e => e.Severity == LogSeverity.Error));
48+
}
49+
50+
public static TheoryData<string[]> ValidExamplesData()
51+
{
52+
return new TheoryData<string[]>
53+
{
54+
// In the following example, the merged object type "Author" is valid. It includes all
55+
// fields from both source schemas, with "age" being hidden due to the @inaccessible
56+
// directive in one of the source schemas.
57+
{
58+
[
59+
"""
60+
# Schema A
61+
type Author {
62+
name: String
63+
age: Int @inaccessible
64+
}
65+
""",
66+
"""
67+
# Schema B
68+
type Author {
69+
age: Int
70+
registered: Boolean
71+
}
72+
"""
73+
]
74+
},
75+
// If the @inaccessible directive is applied to an object type itself, the entire merged
76+
// object type is excluded from the composite execution schema, and it is not required
77+
// to contain any fields.
78+
{
79+
[
80+
"""
81+
# Schema A
82+
type Author @inaccessible {
83+
name: String
84+
age: Int
85+
}
86+
""",
87+
"""
88+
# Schema B
89+
type Author {
90+
registered: Boolean
91+
}
92+
"""
93+
]
94+
},
95+
// The rule does not apply to root operation types.
96+
{
97+
[
98+
"""
99+
# Schema A
100+
type Query {
101+
field: Int @inaccessible
102+
}
103+
104+
type Mutation {
105+
field: Int @inaccessible
106+
}
107+
108+
type Subscription {
109+
field: Int @inaccessible
110+
}
111+
"""
112+
]
113+
}
114+
};
115+
}
116+
117+
public static TheoryData<string[], string[]> InvalidExamplesData()
118+
{
119+
return new TheoryData<string[], string[]>
120+
{
121+
// This example demonstrates an invalid merged object type. In this case, "Author" is
122+
// defined in two source schemas, but all fields are marked as @inaccessible in at least
123+
// one of the source schemas, resulting in an empty merged object type.
124+
{
125+
[
126+
"""
127+
# Schema A
128+
type Author {
129+
name: String @inaccessible
130+
registered: Boolean
131+
}
132+
""",
133+
"""
134+
# Schema B
135+
type Author {
136+
name: String
137+
registered: Boolean @inaccessible
138+
}
139+
"""
140+
],
141+
[
142+
"The merged object type 'Author' is empty."
143+
]
144+
}
145+
};
146+
}
147+
}

0 commit comments

Comments
 (0)