Skip to content

Commit 819f23c

Browse files
authored
[Fusion] Added requirements argument to @fusion__requires (#8268)
1 parent 10072ed commit 819f23c

File tree

14 files changed

+110
-9
lines changed

14 files changed

+110
-9
lines changed

src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Definitions/FusionRequiresMutableDirectiveDefinition.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal sealed class FusionRequiresMutableDirectiveDefinition : MutableDirectiv
1313
{
1414
public FusionRequiresMutableDirectiveDefinition(
1515
MutableEnumTypeDefinition schemaMutableEnumType,
16+
MutableScalarTypeDefinition fieldSelectionSetType,
1617
MutableScalarTypeDefinition fieldDefinitionType,
1718
MutableScalarTypeDefinition fieldSelectionMapType)
1819
: base(FusionRequires)
@@ -25,6 +26,12 @@ public FusionRequiresMutableDirectiveDefinition(
2526
Description = FusionRequiresMutableDirectiveDefinition_Schema_Description
2627
});
2728

29+
Arguments.Add(
30+
new MutableInputFieldDefinition(Requirements, new NonNullType(fieldSelectionSetType))
31+
{
32+
Description = FusionRequiresMutableDirectiveDefinition_Requirements_Description
33+
});
34+
2835
Arguments.Add(
2936
new MutableInputFieldDefinition(Field, new NonNullType(fieldDefinitionType))
3037
{

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
@@ -96,6 +96,9 @@
9696
<data name="FusionRequiresMutableDirectiveDefinition_Map_Description" xml:space="preserve">
9797
<value>The map describes how the argument values for the source schema are resolved from the arguments of the field exposed in the client-facing composite schema and from required data relative to the current type.</value>
9898
</data>
99+
<data name="FusionRequiresMutableDirectiveDefinition_Requirements_Description" xml:space="preserve">
100+
<value>A selection set on the annotated field that describes its requirements.</value>
101+
</data>
99102
<data name="FusionRequiresMutableDirectiveDefinition_Schema_Description" xml:space="preserve">
100103
<value>The name of the source schema where this field has requirements to data on other source schemas.</value>
101104
</data>

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,8 @@ private MutableInterfaceTypeDefinition MergeInterfaceTypes(
405405
.SelectMany(
406406
i => ((MutableInterfaceTypeDefinition)i.Type).Fields.AsEnumerable(),
407407
(i, f) => new OutputFieldInfo(f, (MutableComplexTypeDefinition)i.Type, i.Schema))
408-
.GroupBy(i => i.Field.Name);
408+
.GroupBy(i => i.Field.Name)
409+
.ToImmutableArray();
409410

410411
foreach (var fieldGroup in fieldGroupByName)
411412
{
@@ -417,6 +418,14 @@ private MutableInterfaceTypeDefinition MergeInterfaceTypes(
417418
}
418419
}
419420

421+
foreach (var (fieldName, fieldGroup) in fieldGroupByName)
422+
{
423+
if (interfaceType.Fields.TryGetField(fieldName, out var outputField))
424+
{
425+
AddFusionRequiresDirectives(outputField, interfaceType, [.. fieldGroup]);
426+
}
427+
}
428+
420429
return interfaceType;
421430
}
422431

@@ -485,7 +494,8 @@ .. typeGroup.Where(
485494
.SelectMany(
486495
i => ((MutableObjectTypeDefinition)i.Type).Fields.AsEnumerable(),
487496
(i, f) => new OutputFieldInfo(f, (MutableComplexTypeDefinition)i.Type, i.Schema))
488-
.GroupBy(i => i.Field.Name);
497+
.GroupBy(i => i.Field.Name)
498+
.ToImmutableArray();
489499

490500
foreach (var fieldGroup in fieldGroupByName)
491501
{
@@ -497,6 +507,14 @@ .. typeGroup.Where(
497507
}
498508
}
499509

510+
foreach (var (fieldName, fieldGroup) in fieldGroupByName)
511+
{
512+
if (objectType.Fields.TryGetField(fieldName, out var outputField))
513+
{
514+
AddFusionRequiresDirectives(outputField, objectType, [.. fieldGroup]);
515+
}
516+
}
517+
500518
return objectType;
501519
}
502520

@@ -560,7 +578,6 @@ .. typeGroup.Where(
560578
}
561579

562580
AddFusionFieldDirectives(outputField, fieldGroup);
563-
AddFusionRequiresDirectives(outputField, fieldGroup);
564581

565582
if (fieldGroup.Any(i => i.Field.HasInaccessibleDirective()))
566583
{
@@ -1002,6 +1019,7 @@ private static List<string> GetFusionLookupMap(MutableOutputFieldDefinition fiel
10021019

10031020
private void AddFusionRequiresDirectives(
10041021
MutableOutputFieldDefinition field,
1022+
MutableComplexTypeDefinition complexType,
10051023
ImmutableArray<OutputFieldInfo> fieldGroup)
10061024
{
10071025
foreach (var (sourceField, _, sourceSchema) in fieldGroup)
@@ -1025,6 +1043,22 @@ private void AddFusionRequiresDirectives(
10251043
if (map.Any(v => v is not null))
10261044
{
10271045
var schemaArgument = new EnumValueNode(_schemaConstantNames[sourceSchema]);
1046+
var requiresMap = map.Where(f => f is not null);
1047+
var selectedValues =
1048+
requiresMap.Select(a => new FieldSelectionMapParser(a).Parse());
1049+
var selectedValueToSelectionSetRewriter =
1050+
GetSelectedValueToSelectionSetRewriter(sourceSchema);
1051+
var selectionSets = selectedValues
1052+
.Select(
1053+
s =>
1054+
selectedValueToSelectionSetRewriter
1055+
.SelectedValueToSelectionSet(s, complexType))
1056+
.ToImmutableArray();
1057+
var mergedSelectionSet = selectionSets.Length == 1
1058+
? selectionSets[0]
1059+
: GetMergeSelectionSetRewriter(sourceSchema).Merge(selectionSets, complexType);
1060+
var requirementsArgument =
1061+
mergedSelectionSet.ToString(indented: false).AsSpan()[2 .. ^2].ToString();
10281062

10291063
var fieldArgument =
10301064
_removeDirectivesRewriter
@@ -1039,6 +1073,7 @@ private void AddFusionRequiresDirectives(
10391073
new Directive(
10401074
_fusionDirectiveDefinitions[DirectiveNames.FusionRequires],
10411075
new ArgumentAssignment(ArgumentNames.Schema, schemaArgument),
1076+
new ArgumentAssignment(ArgumentNames.Requirements, requirementsArgument),
10421077
new ArgumentAssignment(ArgumentNames.Field, fieldArgument),
10431078
new ArgumentAssignment(ArgumentNames.Map, mapArgument)));
10441079
}
@@ -1155,6 +1190,7 @@ private FrozenDictionary<string, MutableDirectiveDefinition> CreateFusionDirecti
11551190
DirectiveNames.FusionRequires,
11561191
new FusionRequiresMutableDirectiveDefinition(
11571192
schemaEnumType,
1193+
fieldSelectionSetType,
11581194
fieldDefinitionType,
11591195
fieldSelectionMapType)
11601196
},

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal static class WellKnownArgumentNames
1212
public const string Partial = "partial";
1313
public const string Path = "path";
1414
public const string Provides = "provides";
15+
public const string Requirements = "requirements";
1516
public const string Schema = "schema";
1617
public const string SourceType = "sourceType";
1718
public const string Value = "value";

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ namespace HotChocolate.Fusion.Types.Directives;
66
/*
77
directive @fusion__requires(
88
schema: fusion__Schema!
9+
requirements: fusion__FieldSelectionSet!
910
field: fusion__FieldDefinition!
1011
map: [fusion__FieldSelectionMap]!
1112
) repeatable on FIELD_DEFINITION
1213
*/
1314
internal class RequireDirective(
1415
string schemaName,
16+
SelectionSetNode requirements,
1517
FieldDefinitionNode field,
1618
ImmutableArray<string?> map)
1719
{
@@ -20,6 +22,9 @@ internal class RequireDirective(
2022
/// </summary>
2123
public string SchemaName { get; } = schemaName;
2224

25+
/// <summary>Gets the requirements for a field.</summary>
26+
public SelectionSetNode Requirements { get; } = requirements;
27+
2328
/// <summary>
2429
/// Gets the arguments that represent field requirements.
2530
/// </summary>

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public static bool CanParse(DirectiveNode directiveNode)
1212
public static RequireDirective Parse(DirectiveNode directiveNode)
1313
{
1414
string? schemaName = null;
15+
SelectionSetNode? requirements = null;
1516
FieldDefinitionNode? field = null;
1617
ImmutableArray<string?>? map = null;
1718

@@ -23,6 +24,17 @@ public static RequireDirective Parse(DirectiveNode directiveNode)
2324
schemaName = ((EnumValueNode)argument.Value).Value;
2425
break;
2526

27+
case "requirements":
28+
var requirementsSourceText = ((StringValueNode)argument.Value).Value.Trim();
29+
30+
if (!requirementsSourceText.StartsWith('{'))
31+
{
32+
requirementsSourceText = $"{{ {requirementsSourceText} }}";
33+
}
34+
35+
requirements = Utf8GraphQLParser.Syntax.ParseSelectionSet(requirementsSourceText);
36+
break;
37+
2638
case "field":
2739
field = Utf8GraphQLParser.Syntax.ParseFieldDefinition(((StringValueNode)argument.Value).Value);
2840
break;
@@ -43,6 +55,12 @@ public static RequireDirective Parse(DirectiveNode directiveNode)
4355
"The `schema` argument is required on the @require directive.");
4456
}
4557

58+
if (requirements is null)
59+
{
60+
throw new DirectiveParserException(
61+
"The `requirements` argument is required on the @require directive.");
62+
}
63+
4664
if (field is null)
4765
{
4866
throw new DirectiveParserException(
@@ -55,7 +73,7 @@ public static RequireDirective Parse(DirectiveNode directiveNode)
5573
"The `map` argument is required on the @require directive.");
5674
}
5775

58-
return new RequireDirective(schemaName, field, map.Value);
76+
return new RequireDirective(schemaName, requirements, field, map.Value);
5977
}
6078

6179
private static ImmutableArray<string?> ParseMap(IValueNode value)

src/HotChocolate/Fusion-vnext/test/Fusion.CommandLine.Tests/__resources__/invalid-example-1/result/composite-schema.graphqls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ directive @fusion__inputField("The name of the source schema that originally pro
5656
directive @fusion__lookup("The GraphQL field definition in the source schema that can be used to look up the entity." field: fusion__FieldDefinition! "A selection set on the annotated entity type that describes the stable key for the lookup." key: fusion__FieldSelectionSet! "The map describes how the key values are resolved from the annotated entity type." map: [fusion__FieldSelectionMap!]! "The path to the lookup field relative to the Query type." path: fusion__FieldSelectionPath "The name of the source schema where the annotated entity type can be looked up from." schema: fusion__Schema!) repeatable on OBJECT | INTERFACE | UNION
5757

5858
"The @fusion__requires directive specifies if a field has requirements on a source schema."
59-
directive @fusion__requires("The GraphQL field definition in the source schema that this field depends on." field: fusion__FieldDefinition! "The map describes how the argument values for the source schema are resolved from the arguments of the field exposed in the client-facing composite schema and from required data relative to the current type." map: [fusion__FieldSelectionMap]! "The name of the source schema where this field has requirements to data on other source schemas." schema: fusion__Schema!) repeatable on FIELD_DEFINITION
59+
directive @fusion__requires("The GraphQL field definition in the source schema that this field depends on." field: fusion__FieldDefinition! "The map describes how the argument values for the source schema are resolved from the arguments of the field exposed in the client-facing composite schema and from required data relative to the current type." map: [fusion__FieldSelectionMap]! "A selection set on the annotated field that describes its requirements." requirements: fusion__FieldSelectionSet! "The name of the source schema where this field has requirements to data on other source schemas." schema: fusion__Schema!) repeatable on FIELD_DEFINITION
6060

6161
"The @fusion__type directive specifies which source schemas provide parts of a composite type."
6262
directive @fusion__type("The name of the source schema that originally provided part of the annotated type." schema: fusion__Schema!) repeatable on SCALAR | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT

src/HotChocolate/Fusion-vnext/test/Fusion.CommandLine.Tests/__resources__/valid-example-1/result/composite-schema.graphqls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ directive @fusion__inputField("The name of the source schema that originally pro
4040
directive @fusion__lookup("The GraphQL field definition in the source schema that can be used to look up the entity." field: fusion__FieldDefinition! "A selection set on the annotated entity type that describes the stable key for the lookup." key: fusion__FieldSelectionSet! "The map describes how the key values are resolved from the annotated entity type." map: [fusion__FieldSelectionMap!]! "The path to the lookup field relative to the Query type." path: fusion__FieldSelectionPath "The name of the source schema where the annotated entity type can be looked up from." schema: fusion__Schema!) repeatable on OBJECT | INTERFACE | UNION
4141

4242
"The @fusion__requires directive specifies if a field has requirements on a source schema."
43-
directive @fusion__requires("The GraphQL field definition in the source schema that this field depends on." field: fusion__FieldDefinition! "The map describes how the argument values for the source schema are resolved from the arguments of the field exposed in the client-facing composite schema and from required data relative to the current type." map: [fusion__FieldSelectionMap]! "The name of the source schema where this field has requirements to data on other source schemas." schema: fusion__Schema!) repeatable on FIELD_DEFINITION
43+
directive @fusion__requires("The GraphQL field definition in the source schema that this field depends on." field: fusion__FieldDefinition! "The map describes how the argument values for the source schema are resolved from the arguments of the field exposed in the client-facing composite schema and from required data relative to the current type." map: [fusion__FieldSelectionMap]! "A selection set on the annotated field that describes its requirements." requirements: fusion__FieldSelectionSet! "The name of the source schema where this field has requirements to data on other source schemas." schema: fusion__Schema!) repeatable on FIELD_DEFINITION
4444

4545
"The @fusion__type directive specifies which source schemas provide parts of a composite type."
4646
directive @fusion__type("The name of the source schema that originally provided part of the annotated type." schema: fusion__Schema!) repeatable on SCALAR | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT

src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/SourceSchemaMerger.Argument.Tests.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,18 @@ type Query
161161
"""
162162
type Product {
163163
id: ID!
164+
dimension: ProductDimension!
164165
delivery(
165166
zip: String!
166167
size: Int! @require(field: "dimension.size")
167168
weight: Int! @require(field: "dimension.weight")
168169
): DeliveryEstimates
169170
}
171+
172+
type ProductDimension {
173+
size: Int!
174+
weight: Int!
175+
}
170176
"""
171177
],
172178
"""
@@ -175,11 +181,21 @@ type Product
175181
delivery(zip: String!
176182
@fusion__inputField(schema: A)): DeliveryEstimates
177183
@fusion__field(schema: A)
178-
@fusion__requires(schema: A, field: "delivery(zip: String! size: Int! weight: Int!): DeliveryEstimates", map: [ null, "dimension.size", "dimension.weight" ])
184+
@fusion__requires(schema: A, requirements: "dimension { size weight }", field: "delivery(zip: String! size: Int! weight: Int!): DeliveryEstimates", map: [ null, "dimension.size", "dimension.weight" ])
185+
dimension: ProductDimension!
186+
@fusion__field(schema: A)
179187
id: ID!
180188
@fusion__field(schema: A)
181189
}
182190
191+
type ProductDimension
192+
@fusion__type(schema: A) {
193+
size: Int!
194+
@fusion__field(schema: A)
195+
weight: Int!
196+
@fusion__field(schema: A)
197+
}
198+
183199
scalar DeliveryEstimates
184200
@fusion__type(schema: A)
185201
"""

src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/SourceSchemaMerger.OutputField.Tests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ type Product
129129
"""
130130
# Schema A
131131
type Product {
132+
percent: Int
132133
discountPercentage(percent: Int): Int
133134
}
134135
""",
@@ -146,7 +147,9 @@ type Product
146147
discountPercentage: Int
147148
@fusion__field(schema: A)
148149
@fusion__field(schema: B)
149-
@fusion__requires(schema: B, field: "discountPercentage(percent: Int): Int", map: [ "percent" ])
150+
@fusion__requires(schema: B, requirements: "percent", field: "discountPercentage(percent: Int): Int", map: [ "percent" ])
151+
percent: Int
152+
@fusion__field(schema: A)
150153
}
151154
"""
152155
},

src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/__snapshots__/SourceSchemaMergerTests.Merge_FourNamedSchemas_AddsFusionDefinitions.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ directive @fusion__inputField("The name of the source schema that originally pro
2929
directive @fusion__lookup("The GraphQL field definition in the source schema that can be used to look up the entity." field: fusion__FieldDefinition! "A selection set on the annotated entity type that describes the stable key for the lookup." key: fusion__FieldSelectionSet! "The map describes how the key values are resolved from the annotated entity type." map: [fusion__FieldSelectionMap!]! "The path to the lookup field relative to the Query type." path: fusion__FieldSelectionPath "The name of the source schema where the annotated entity type can be looked up from." schema: fusion__Schema!) repeatable on OBJECT | INTERFACE | UNION
3030

3131
"The @fusion__requires directive specifies if a field has requirements on a source schema."
32-
directive @fusion__requires("The GraphQL field definition in the source schema that this field depends on." field: fusion__FieldDefinition! "The map describes how the argument values for the source schema are resolved from the arguments of the field exposed in the client-facing composite schema and from required data relative to the current type." map: [fusion__FieldSelectionMap]! "The name of the source schema where this field has requirements to data on other source schemas." schema: fusion__Schema!) repeatable on FIELD_DEFINITION
32+
directive @fusion__requires("The GraphQL field definition in the source schema that this field depends on." field: fusion__FieldDefinition! "The map describes how the argument values for the source schema are resolved from the arguments of the field exposed in the client-facing composite schema and from required data relative to the current type." map: [fusion__FieldSelectionMap]! "A selection set on the annotated field that describes its requirements." requirements: fusion__FieldSelectionSet! "The name of the source schema where this field has requirements to data on other source schemas." schema: fusion__Schema!) repeatable on FIELD_DEFINITION
3333

3434
"The @fusion__type directive specifies which source schemas provide parts of a composite type."
3535
directive @fusion__type("The name of the source schema that originally provided part of the annotated type." schema: fusion__Schema!) repeatable on SCALAR | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT

0 commit comments

Comments
 (0)