Skip to content

Commit aa214e1

Browse files
authored
Adds support for nested lookups (#8884)
1 parent f3a4b80 commit aa214e1

File tree

10 files changed

+497
-5
lines changed

10 files changed

+497
-5
lines changed

src/HotChocolate/Core/src/Types/Types/Composite/Directives/ShareableAttribute.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ namespace HotChocolate.Types.Composite;
3333
/// </para>
3434
/// </summary>
3535
[AttributeUsage(
36-
AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property)]
36+
AttributeTargets.Class
37+
| AttributeTargets.Struct
38+
| AttributeTargets.Method
39+
| AttributeTargets.Property)]
3740
public sealed class ShareableAttribute : DescriptorAttribute
3841
{
3942
public ShareableAttribute()

src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Completion/CompletionTools.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ private static ImmutableArray<Lookup> GetLookupBySchema(
214214
lookup.Field.Type.NamedType().Name.Value,
215215
lookup.Internal,
216216
arguments.ToImmutable(),
217-
fields));
217+
fields,
218+
lookup.Path));
218219
}
219220
}
220221

src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Directives/LookupDirectiveParser.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,8 @@ public static LookupDirective Parse(DirectiveNode directive)
4848
case "path":
4949
if (argument.Value is StringValueNode pathValueNode)
5050
{
51-
path = pathValueNode.Value.Trim().Split('.').ToImmutableArray();
51+
path = [..pathValueNode.Value.Trim().Split('.')];
5252
}
53-
5453
break;
5554

5655
case "internal":

src/HotChocolate/Fusion-vnext/src/Fusion.Execution.Types/Metadata/Lookup.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public sealed class Lookup : INeedsCompletion
2626
/// <param name="isInternal">Whether the lookup is internal or not.</param>
2727
/// <param name="arguments">The arguments that represent field requirements.</param>
2828
/// <param name="fields">The paths to the field that are required.</param>
29+
/// <param name="path">
30+
/// The path to the lookup field relative to the Query type.
31+
/// </param>
2932
/// <exception cref="ArgumentException">
3033
/// Thrown when the <paramref name="arguments"/> or <paramref name="fields"/> is empty.
3134
/// </exception>
@@ -40,7 +43,8 @@ public Lookup(
4043
string fieldType,
4144
bool isInternal,
4245
ImmutableArray<LookupArgument> arguments,
43-
ImmutableArray<IValueSelectionNode> fields)
46+
ImmutableArray<IValueSelectionNode> fields,
47+
ImmutableArray<string> path)
4448
{
4549
ArgumentException.ThrowIfNullOrEmpty(schemaName);
4650
ArgumentException.ThrowIfNullOrEmpty(declaringTypeName);
@@ -64,6 +68,7 @@ public Lookup(
6468
IsInternal = isInternal;
6569
Arguments = arguments;
6670
Fields = fields;
71+
Path = path;
6772
}
6873

6974
/// <summary>
@@ -96,6 +101,11 @@ public Lookup(
96101
/// </summary>
97102
public ImmutableArray<IValueSelectionNode> Fields { get; }
98103

104+
/// <summary>
105+
/// Gets the path to the lookup field relative to the Query type.
106+
/// </summary>
107+
public ImmutableArray<string> Path { get; set; }
108+
99109
/// <summary>
100110
/// Gets the data requirements for this lookup field.
101111
/// </summary>

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationDefinitionBuilder.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ public OperationDefinitionBuilder SetSelectionSet(SelectionSetNode selectionSet)
7676

7777
if (_lookup is not null && _lookupArguments is not null && _typeToLookup is not null)
7878
{
79+
if (!_lookup.Path.IsEmpty)
80+
{
81+
foreach (var fieldName in _lookup.Path)
82+
{
83+
selectionPathBuilder.AppendField(fieldName);
84+
}
85+
}
86+
7987
selectionPathBuilder.AppendField(_lookup.FieldName);
8088

8189
var lookupSelectionSet = selectionSet;
@@ -111,6 +119,17 @@ public OperationDefinitionBuilder SetSelectionSet(SelectionSetNode selectionSet)
111119
_lookupArguments,
112120
lookupSelectionSet);
113121

122+
if (!_lookup.Path.IsEmpty)
123+
{
124+
for (var i = _lookup.Path.Length - 1; i >= 0; i--)
125+
{
126+
var fieldName = _lookup.Path[i];
127+
var fieldSelectionSet = new SelectionSetNode(null, [lookupField]);
128+
lookupField = new FieldNode(fieldName, fieldSelectionSet);
129+
indexBuilder.Register(fieldSelectionSet);
130+
}
131+
}
132+
114133
selectionSet = new SelectionSetNode(null, [lookupField]);
115134
}
116135

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,22 @@ public static bool AreAllProvidedSelectionsConditional(this OperationPlanStep st
705705
{
706706
FieldNode? lookupFieldNode = null;
707707

708+
if (!step.Lookup.Path.IsEmpty)
709+
{
710+
foreach (var fieldName in step.Lookup.Path)
711+
{
712+
var fieldNode = selectionSetNode.Selections.FirstOrDefault(
713+
selection => selection is FieldNode fieldNode && fieldNode.Name.Value == fieldName);
714+
715+
if (fieldNode is not FieldNode { SelectionSet: { } nextSelectionSetNode })
716+
{
717+
throw new InvalidOperationException("Unable to resolve the lookup path.");
718+
}
719+
720+
selectionSetNode = nextSelectionSetNode;
721+
}
722+
}
723+
708724
foreach (var selection in selectionSetNode.Selections)
709725
{
710726
if (selection is FieldNode fieldNode && fieldNode.Name.Value == step.Lookup.FieldName)
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using HotChocolate.Transport;
2+
using HotChocolate.Transport.Http;
3+
using HotChocolate.Types;
4+
using HotChocolate.Types.Composite;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace HotChocolate.Fusion;
8+
9+
public class LookupTests : FusionTestBase
10+
{
11+
[Fact]
12+
public async Task Fetch_From_Nested_Internal_Lookup()
13+
{
14+
// arrange
15+
using var server1 = CreateSourceSchema(
16+
"a",
17+
b => b.AddQueryType<NestedLookups.SourceSchema1.Query>());
18+
19+
using var server2 = CreateSourceSchema(
20+
"b",
21+
b => b.AddQueryType<NestedLookups.SourceSchema2.Query>());
22+
23+
using var gateway = await CreateCompositeSchemaAsync(
24+
[
25+
("a", server1),
26+
("b", server2)
27+
]);
28+
29+
// act
30+
using var client = GraphQLHttpClient.Create(gateway.CreateClient());
31+
32+
var request = new OperationRequest(
33+
"""
34+
{
35+
books {
36+
author {
37+
name
38+
}
39+
}
40+
}
41+
""");
42+
43+
using var result = await client.PostAsync(
44+
request,
45+
new Uri("http://localhost:5000/graphql"));
46+
47+
// assert
48+
await MatchSnapshotAsync(gateway, request, result);
49+
}
50+
51+
public static class NestedLookups
52+
{
53+
public static class SourceSchema1
54+
{
55+
public record Book(int Id, string Title, [property: Shareable] Author Author);
56+
57+
[EntityKey("id")]
58+
public record Author(int Id);
59+
60+
public class Query
61+
{
62+
private readonly OrderedDictionary<int, Book> _books =
63+
new()
64+
{
65+
[1] = new Book(1, "C# in Depth", new Author(1)),
66+
[2] = new Book(2, "The Lord of the Rings", new Author(2)),
67+
[3] = new Book(3, "The Hobbit", new Author(2)),
68+
[4] = new Book(4, "The Silmarillion", new Author(2))
69+
};
70+
71+
public IEnumerable<Book> GetBooks()
72+
=> _books.Values;
73+
}
74+
}
75+
76+
public static class SourceSchema2
77+
{
78+
public record Author(int Id, string Name);
79+
80+
public class Query
81+
{
82+
[Internal]
83+
public InternalLookups Lookups { get; } = new();
84+
}
85+
86+
[Internal]
87+
public class InternalLookups
88+
{
89+
private readonly OrderedDictionary<int, Author> _authors = new()
90+
{
91+
[1] = new Author(1, "Jon Skeet"),
92+
[2] = new Author(2, "JRR Tolkien")
93+
};
94+
95+
[Lookup]
96+
public Author GetAuthorById(int id)
97+
=> _authors[id];
98+
}
99+
100+
public record Book(int Id, [property: Shareable] Author Author)
101+
{
102+
public string IdAndTitle([Require] string title)
103+
=> $"{Id} - {title}";
104+
}
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)