Skip to content

Commit a123dc8

Browse files
authored
[Fusion] Add InterfaceObject support (#10079)
1 parent 24c04a1 commit a123dc8

225 files changed

Lines changed: 16282 additions & 586 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/All.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@
229229
<Project Path="HotChocolate/Fusion/src/Fusion.SourceSchema.Packaging/HotChocolate.Fusion.SourceSchema.Packaging.csproj" />
230230
</Folder>
231231
<Folder Name="/HotChocolate/Fusion/test/">
232+
<Project Path="HotChocolate/Fusion/test/Fusion.Aspire.Tests/HotChocolate.Fusion.Aspire.Tests.csproj" />
232233
<Project Path="HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj" />
233234
<Project Path="HotChocolate/Fusion/test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj" />
234235
<Project Path="HotChocolate/Fusion/test/Fusion.Composition.ApolloFederation.Tests/HotChocolate.Fusion.Composition.ApolloFederation.Tests.csproj" />

src/HotChocolate/Core/src/Execution.Operation.Abstractions/ISelectionSet.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,13 @@ public interface ISelectionSet
3535
bool HasIncrementalParts { get; }
3636

3737
/// <summary>
38-
/// Gets the type that declares this selection set.
38+
/// Gets the complex type that declares this selection set.
3939
/// </summary>
40-
IObjectTypeDefinition Type { get; }
40+
/// <remarks>
41+
/// This is typically an object type. It can be an interface type when the concrete runtime
42+
/// type of a selection set is not yet known.
43+
/// </remarks>
44+
IComplexTypeDefinition Type { get; }
4145

4246
/// <summary>
4347
/// Gets the selections that shall be executed.

src/HotChocolate/Core/src/Types/Execution/Processing/SelectionSet.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ internal SelectionSet(
6767
/// </summary>
6868
public IObjectTypeDefinition Type { get; }
6969

70+
IComplexTypeDefinition ISelectionSet.Type => Type;
71+
7072
/// <summary>
7173
/// Gets the declaring operation.
7274
/// </summary>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace HotChocolate.Types.Composite;
2+
3+
/// <summary>
4+
/// <para>
5+
/// The @external directive indicates that a field is recognized by the current source schema but is
6+
/// not directly contributed (resolved) by it. Instead, the source schema references the field for
7+
/// specific composition purposes, for example as part of a @requires selection.
8+
/// </para>
9+
/// <para>
10+
/// <see href="https://graphql.github.io/composite-schemas-spec/draft/#sec--external"/>
11+
/// </para>
12+
/// <code>
13+
/// type User @key(fields: "id") {
14+
/// id: ID!
15+
/// name: String! @external
16+
/// username: String! @requires(fields: "name")
17+
/// }
18+
///
19+
/// directive @external on FIELD_DEFINITION
20+
/// </code>
21+
/// </summary>
22+
[DirectiveType(
23+
DirectiveNames.External.Name,
24+
DirectiveLocation.FieldDefinition,
25+
IsRepeatable = false)]
26+
public sealed class External
27+
{
28+
private External()
29+
{
30+
}
31+
32+
/// <inheritdoc />
33+
public override string ToString() => "@external";
34+
35+
/// <summary>
36+
/// The singleton instance of the <see cref="External"/> directive.
37+
/// </summary>
38+
public static External Instance { get; } = new();
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace HotChocolate.Types.Composite;
2+
3+
/// <summary>
4+
/// <para>
5+
/// The @implement directive marks a field as an explicit implementation that intentionally replaces
6+
/// a default field implementation contributed to an interface by an @interfaceObject stand-in. It is
7+
/// required on an implementing type's field, or on a more specific interface's own stand-in field,
8+
/// that collides with an applicable default.
9+
/// </para>
10+
/// <para>
11+
/// <see href="https://graphql.github.io/composite-schemas-spec/draft/#sec--implement"/>
12+
/// </para>
13+
/// <code>
14+
/// type Chair implements Product {
15+
/// id: ID!
16+
/// taxRate: Float! @implement
17+
/// }
18+
///
19+
/// directive @implement on FIELD_DEFINITION
20+
/// </code>
21+
/// </summary>
22+
[DirectiveType(
23+
DirectiveNames.Implement.Name,
24+
DirectiveLocation.FieldDefinition,
25+
IsRepeatable = false)]
26+
public sealed class Implement
27+
{
28+
private Implement()
29+
{
30+
}
31+
32+
/// <inheritdoc />
33+
public override string ToString() => "@implement";
34+
35+
/// <summary>
36+
/// The singleton instance of the <see cref="Implement"/> directive.
37+
/// </summary>
38+
public static Implement Instance { get; } = new();
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace HotChocolate.Types.Composite;
2+
3+
/// <summary>
4+
/// <para>
5+
/// The @interfaceObject directive is used within a source schema to declare an object type that
6+
/// acts as a stand-in for an interface defined in another source schema. The stand-in carries the
7+
/// same name as the interface and allows a source schema to contribute fields to that interface
8+
/// without defining any of its implementing types.
9+
/// </para>
10+
/// <para>
11+
/// <see href="https://graphql.github.io/composite-schemas-spec/draft/#sec--interfaceObject"/>
12+
/// </para>
13+
/// <code>
14+
/// type Media @interfaceObject @key(fields: "id") {
15+
/// id: ID!
16+
/// reviews: [Review!]!
17+
/// }
18+
///
19+
/// directive @interfaceObject on OBJECT
20+
/// </code>
21+
/// </summary>
22+
[DirectiveType(
23+
DirectiveNames.InterfaceObject.Name,
24+
DirectiveLocation.Object,
25+
IsRepeatable = false)]
26+
public sealed class InterfaceObject
27+
{
28+
private InterfaceObject()
29+
{
30+
}
31+
32+
/// <inheritdoc />
33+
public override string ToString() => "@interfaceObject";
34+
35+
/// <summary>
36+
/// The singleton instance of the <see cref="InterfaceObject"/> directive.
37+
/// </summary>
38+
public static InterfaceObject Instance { get; } = new();
39+
}

src/HotChocolate/Core/test/Execution.Tests/Processing/OperationCompilerTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,33 @@ public void Prepare_One_Field()
3434
MatchSnapshot(document, operation);
3535
}
3636

37+
[Fact]
38+
public void Compile_Should_ExposeObjectSelectionSetThroughPublicContract()
39+
{
40+
// arrange
41+
var schema = SchemaBuilder.New()
42+
.AddQueryType(
43+
c => c
44+
.Name("Query")
45+
.Field("foo")
46+
.Type<StringType>()
47+
.Resolve("foo"))
48+
.Create();
49+
var document = Utf8GraphQLParser.Parse("{ foo }");
50+
51+
// act
52+
var operation = OperationCompiler.Compile("opid", document, schema);
53+
ISelectionSet selectionSet = operation.RootSelectionSet;
54+
55+
// assert
56+
Assert.Equal("Query", selectionSet.Type.Name);
57+
Assert.IsAssignableFrom<IObjectTypeDefinition>(selectionSet.Type);
58+
59+
var selection = Assert.Single(selectionSet.GetSelections());
60+
Assert.Equal("Query", selection.DeclaringSelectionSet.Type.Name);
61+
Assert.IsAssignableFrom<IObjectTypeDefinition>(selection.DeclaringSelectionSet.Type);
62+
}
63+
3764
[Fact]
3865
public void Prepare_Duplicate_Field()
3966
{

src/HotChocolate/Fusion/HotChocolate.Fusion.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<Project Path="src/Fusion.Utilities/HotChocolate.Fusion.Utilities.csproj" />
2424
</Folder>
2525
<Folder Name="/test/">
26+
<Project Path="test/Fusion.Aspire.Tests/HotChocolate.Fusion.Aspire.Tests.csproj" />
2627
<Project Path="test/Fusion.AspNetCore.Tests/HotChocolate.Fusion.AspNetCore.Tests.csproj" />
2728
<Project Path="test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj" />
2829
<Project Path="test/Fusion.Composition.ApolloFederation.Tests/HotChocolate.Fusion.Composition.ApolloFederation.Tests.csproj" />

src/HotChocolate/Fusion/src/Fusion.Aspire/AspireCompositionHelper.cs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,7 @@ public static async Task<bool> TryComposeAsync(
2121

2222
var compositionLog = new CompositionLog();
2323
var environment = settings.EnvironmentName ?? "Aspire";
24-
var compositionSettings = new CompositionSettings
25-
{
26-
Merger = { EnableGlobalObjectIdentification = settings.EnableGlobalObjectIdentification },
27-
Satisfiability = { IncludeSatisfiabilityPaths = settings.IncludeSatisfiabilityPaths },
28-
Preprocessor = { ExcludeByTag = settings.ExcludeByTag?.ToHashSet() }
29-
};
24+
var compositionSettings = CreateCompositionSettings(settings);
3025
var sourceSchemas = newSourceSchemas.ToDictionary(
3126
s => s.Name,
3227
s => (s.Schema, s.SchemaSettings));
@@ -67,6 +62,26 @@ public static async Task<bool> TryComposeAsync(
6762
return true;
6863
}
6964

65+
internal static CompositionSettings CreateCompositionSettings(
66+
GraphQLCompositionSettings settings)
67+
{
68+
return new CompositionSettings
69+
{
70+
Merger =
71+
{
72+
EnableGlobalObjectIdentification = settings.EnableGlobalObjectIdentification,
73+
NodeResolution = settings.NodeResolution
74+
},
75+
Satisfiability = { IncludeSatisfiabilityPaths = settings.IncludeSatisfiabilityPaths },
76+
ApolloFederationCompatibility =
77+
{
78+
AllowNonResolvableInterfaceObjects = settings.AllowNonResolvableInterfaceObjects,
79+
ShareableFieldRuntimeTypeRouting = settings.ShareableFieldRuntimeTypeRouting
80+
},
81+
Preprocessor = { ExcludeByTag = settings.ExcludeByTag?.ToHashSet() }
82+
};
83+
}
84+
7085
/// <summary>
7186
/// Since we're prefixing the message with an emoji and space before printing,
7287
/// we need to also indent each line of a multiline message by three spaces to fix the alignment.

src/HotChocolate/Fusion/src/Fusion.Aspire/GraphQLCompositionSettings.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,29 @@ public struct GraphQLCompositionSettings
1010
/// </summary>
1111
public bool? EnableGlobalObjectIdentification { get; set; }
1212

13+
/// <summary>
14+
/// Gets or sets how the gateway resolves the <c>Query.node</c> field.
15+
/// </summary>
16+
public NodeResolution? NodeResolution { get; set; }
17+
1318
/// <summary>
1419
/// Gets or sets a value indicating whether satisfiability paths should be included in the
1520
/// composed schema.
1621
/// </summary>
1722
public bool? IncludeSatisfiabilityPaths { get; set; }
1823

24+
/// <summary>
25+
/// Gets or sets whether Apollo Federation non-resolvable interface objects are accepted and
26+
/// unresolved projected fields are reported as field errors at runtime.
27+
/// </summary>
28+
public bool? AllowNonResolvableInterfaceObjects { get; set; }
29+
30+
/// <summary>
31+
/// Gets or sets how runtime types are routed for Apollo Federation shareable fields whose
32+
/// result type is abstract.
33+
/// </summary>
34+
public ShareableFieldRuntimeTypeRouting? ShareableFieldRuntimeTypeRouting { get; set; }
35+
1936
/// <summary>
2037
/// Gets or sets the set of tags whose annotated schema elements shall be excluded from
2138
/// composition.

0 commit comments

Comments
 (0)