diff --git a/src/HotChocolate/Fusion/src/Composition/Directives/DirectivesHelper.cs b/src/HotChocolate/Fusion/src/Composition/Directives/DirectivesHelper.cs index 250e0c8f040..93e7e73f3e8 100644 --- a/src/HotChocolate/Fusion/src/Composition/Directives/DirectivesHelper.cs +++ b/src/HotChocolate/Fusion/src/Composition/Directives/DirectivesHelper.cs @@ -8,6 +8,7 @@ namespace HotChocolate.Fusion.Composition; internal static class DirectivesHelper { public const string IsDirectiveName = "is"; + public const string RequireDirectiveName = "require"; public const string RemoveDirectiveName = "remove"; public const string RenameDirectiveName = "rename"; public const string CoordinateArg = "coordinate"; @@ -38,4 +39,21 @@ public static IsDirective GetIsDirective(this IHasDirectives member) throw new InvalidOperationException( DirectivesHelper_GetIsDirective_NoFieldAndNoCoordinate); } + + public static bool ContainsRequireDirective(this IHasDirectives member) + => member.Directives.ContainsName(RequireDirectiveName); + + public static RequireDirective GetRequireDirective(this IHasDirectives member) + { + var directive = member.Directives[RequireDirectiveName].First(); + var arg = directive.Arguments.FirstOrDefault(t => t.Name.EqualsOrdinal(FieldArg)); + + if (arg is { Value: StringValueNode field }) + { + return new RequireDirective(Utf8GraphQLParser.Syntax.ParseField(field.Value)); + } + + throw new InvalidOperationException( + DirectivesHelper_GetRequireDirective_NoFieldArg); + } } diff --git a/src/HotChocolate/Fusion/src/Composition/Directives/RequireDirective.cs b/src/HotChocolate/Fusion/src/Composition/Directives/RequireDirective.cs new file mode 100644 index 00000000000..b2aa7288a4a --- /dev/null +++ b/src/HotChocolate/Fusion/src/Composition/Directives/RequireDirective.cs @@ -0,0 +1,13 @@ +using HotChocolate.Language; + +namespace HotChocolate.Fusion.Composition; + +internal sealed class RequireDirective +{ + public RequireDirective(FieldNode field) + { + Field = field; + } + + public FieldNode Field { get; } +} diff --git a/src/HotChocolate/Fusion/src/Composition/Entities/FieldDependency.cs b/src/HotChocolate/Fusion/src/Composition/Entities/FieldDependency.cs index f0afb7cd4ea..71ce189d68a 100644 --- a/src/HotChocolate/Fusion/src/Composition/Entities/FieldDependency.cs +++ b/src/HotChocolate/Fusion/src/Composition/Entities/FieldDependency.cs @@ -1,7 +1,8 @@ -using HotChocolate.Skimmed; - namespace HotChocolate.Fusion.Composition; +/// +/// Describes the dependency a field has to another subgraph. +/// internal sealed class FieldDependency { public FieldDependency(int id, string subgraphName) @@ -10,15 +11,19 @@ public FieldDependency(int id, string subgraphName) SubgraphName = subgraphName; } + /// + /// Gets the internal ID of this dependency, + /// public int Id { get; } + /// + /// Gets the name of the subgraph in which it depends on other subgraph data. + /// There might be multiple resolver overloads that have different dependencies. + /// public string SubgraphName { get; } + /// + /// The arguments that represent dependencies. + /// public Dictionary Arguments { get; } = new(); } - -internal sealed record MemberReference(IsDirective Reference, InputField Argument) -{ - public bool IsRequired => Argument.Type.Kind is TypeKind.NonNull; -} - diff --git a/src/HotChocolate/Fusion/src/Composition/Entities/MemberReference.cs b/src/HotChocolate/Fusion/src/Composition/Entities/MemberReference.cs new file mode 100644 index 00000000000..b2ed407cf2e --- /dev/null +++ b/src/HotChocolate/Fusion/src/Composition/Entities/MemberReference.cs @@ -0,0 +1,6 @@ +using HotChocolate.Language; +using HotChocolate.Skimmed; + +namespace HotChocolate.Fusion.Composition; + +internal sealed record MemberReference(InputField Argument, FieldNode Requirement); diff --git a/src/HotChocolate/Fusion/src/Composition/Extensions/SchemaExtensions.cs b/src/HotChocolate/Fusion/src/Composition/Extensions/SchemaExtensions.cs index 523dfa9c2ea..61c3adc1ca9 100644 --- a/src/HotChocolate/Fusion/src/Composition/Extensions/SchemaExtensions.cs +++ b/src/HotChocolate/Fusion/src/Composition/Extensions/SchemaExtensions.cs @@ -24,8 +24,8 @@ public static string CreateVariableName( SchemaCoordinate coordinate) => $"{type.Name}_{coordinate.MemberName}"; - private static string CreateVariableName( - ObjectType type, + public static string CreateVariableName( + this ObjectType type, FieldNode field) { var context = new FieldVariableNameContext(); diff --git a/src/HotChocolate/Fusion/src/Composition/LogEntryHelper.cs b/src/HotChocolate/Fusion/src/Composition/LogEntryHelper.cs index acfe7e117f9..4d2e2d593d6 100644 --- a/src/HotChocolate/Fusion/src/Composition/LogEntryHelper.cs +++ b/src/HotChocolate/Fusion/src/Composition/LogEntryHelper.cs @@ -102,16 +102,6 @@ public static LogEntry OutputFieldArgumentSetMismatch( coordinate: coordinate, member: field); - public static LogEntry CoordinateNotAllowedForRequirements( - SchemaCoordinate coordinate, - Schema schema) - => new LogEntry( - LogEntryHelper_CoordinateNotAllowedForRequirements, - severity: LogSeverity.Error, - code: LogEntryCodes.CoordinateNotAllowedForRequirements, - coordinate: coordinate, - schema: schema); - public static LogEntry FieldDependencyCannotBeResolved( SchemaCoordinate coordinate, FieldNode dependency, diff --git a/src/HotChocolate/Fusion/src/Composition/Pipeline/Enrichers/RequireEnricher.cs b/src/HotChocolate/Fusion/src/Composition/Pipeline/Enrichers/RequireEnricher.cs index 33d7ae57ad5..ee4b33544e1 100644 --- a/src/HotChocolate/Fusion/src/Composition/Pipeline/Enrichers/RequireEnricher.cs +++ b/src/HotChocolate/Fusion/src/Composition/Pipeline/Enrichers/RequireEnricher.cs @@ -17,9 +17,11 @@ public ValueTask EnrichAsync( foreach (var argument in field.Arguments) { - if (argument.ContainsIsDirective()) + if (argument.ContainsRequireDirective()) { - var memberRef = new MemberReference(argument.GetIsDirective(), argument); + var memberRef = new MemberReference( + argument, + argument.GetRequireDirective().Field); dependency ??= new FieldDependency(++nextId, schema.Name); dependency.Arguments.Add(argument.Name, memberRef); } diff --git a/src/HotChocolate/Fusion/src/Composition/Pipeline/EntityFieldDependencyMiddleware.cs b/src/HotChocolate/Fusion/src/Composition/Pipeline/EntityFieldDependencyMiddleware.cs index f58f78456c8..cda1ce714fa 100644 --- a/src/HotChocolate/Fusion/src/Composition/Pipeline/EntityFieldDependencyMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Composition/Pipeline/EntityFieldDependencyMiddleware.cs @@ -99,22 +99,10 @@ private static void ResolveDependencies( supportedBy.Add(subgraph.Name); } - if (memberRef.Reference.IsCoordinate) - { - context.Log.Write( - CoordinateNotAllowedForRequirements( - new SchemaCoordinate( - entityType.Name, - entityField.Name, - argumentName), - context.GetSubgraphSchema(dependency.SubgraphName))); - continue; - } - if (!CanResolve( context, entityType, - memberRef.Reference.Field, + memberRef.Requirement, supportedBy)) { context.Log.Write( @@ -123,12 +111,12 @@ private static void ResolveDependencies( entityType.Name, entityField.Name, argumentName), - memberRef.Reference.Field, + memberRef.Requirement, context.GetSubgraphSchema(dependency.SubgraphName))); continue; } - var argumentRef = entityType.CreateVariableName(memberRef.Reference); + var argumentRef = entityType.CreateVariableName(memberRef.Requirement); argumentRefLookup.Add(argumentName, argumentRef); arguments.Add(argumentRef, memberRef.Argument.Type.ToTypeNode()); @@ -138,7 +126,7 @@ private static void ResolveDependencies( context.FusionTypes.CreateVariableDirective( subgraph, argumentRef, - memberRef.Reference.Field)); + memberRef.Requirement)); } } } diff --git a/src/HotChocolate/Fusion/src/Composition/Properties/CompositionResources.Designer.cs b/src/HotChocolate/Fusion/src/Composition/Properties/CompositionResources.Designer.cs index d3794f4cc2e..6a164b33b03 100644 --- a/src/HotChocolate/Fusion/src/Composition/Properties/CompositionResources.Designer.cs +++ b/src/HotChocolate/Fusion/src/Composition/Properties/CompositionResources.Designer.cs @@ -122,5 +122,11 @@ internal static string CannotFindCorrelatingSubgraphField { return ResourceManager.GetString("CannotFindCorrelatingSubgraphField", resourceCulture); } } + + internal static string DirectivesHelper_GetRequireDirective_NoFieldArg { + get { + return ResourceManager.GetString("DirectivesHelper_GetRequireDirective_NoFieldArg", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Fusion/src/Composition/Properties/CompositionResources.resx b/src/HotChocolate/Fusion/src/Composition/Properties/CompositionResources.resx index d57d6cdc1ae..7032ac4f7fc 100644 --- a/src/HotChocolate/Fusion/src/Composition/Properties/CompositionResources.resx +++ b/src/HotChocolate/Fusion/src/Composition/Properties/CompositionResources.resx @@ -43,7 +43,7 @@ The number of arguments in an output field does not match the number of arguments in the same field in of a subgraph schema. - The is argument must have a value for coordinate or field. + The is directive must have a value for coordinate or field. An output field (`{0}`) that is being merged into another has a different set of arguments. @@ -60,4 +60,7 @@ Source Arguments: {2} Cannot find correlating subgraph field. + + The require directive must have a value for the argument field. + diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Composition.Tests/DemoIntegrationTests.cs index 8b8549a4161..85a0534595e 100644 --- a/src/HotChocolate/Fusion/test/Composition.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Composition.Tests/DemoIntegrationTests.cs @@ -120,48 +120,4 @@ public async Task Accounts_And_Reviews_Products_AutoCompose_With_Node() .FormatAsString(fusionConfig) .MatchSnapshot(extension: ".graphql"); } - - [Fact] - public async Task Accounts_And_Reviews_Products_Shipping_With_Is_Directive() - { - // arrange - using var demoProject = await DemoProject.CreateAsync(); - - var composer = new FusionGraphComposer(logFactory: _logFactory); - - var fusionConfig = await composer.ComposeAsync( - new[] - { - demoProject.Accounts.ToConfiguration(), - demoProject.Reviews.ToConfiguration(), - demoProject.Products.ToConfiguration(ProductsExtensionSdl), - demoProject.Shipping.ToConfiguration(ShippingExtensionSdl), - }); - - SchemaFormatter - .FormatAsString(fusionConfig) - .MatchSnapshot(extension: ".graphql"); - } - - [Fact] - public async Task Accounts_And_Reviews_Products_Shipping_With_Map_Directive() - { - // arrange - using var demoProject = await DemoProject.CreateAsync(); - - var composer = new FusionGraphComposer(logFactory: _logFactory); - - var fusionConfig = await composer.ComposeAsync( - new[] - { - demoProject.Accounts.ToConfiguration(), - demoProject.Reviews.ToConfiguration(), - demoProject.Products.ToConfiguration(ProductsExtensionSdl), - demoProject.Shipping.ToConfiguration(ShippingExtensionSdl2), - }); - - SchemaFormatter - .FormatAsString(fusionConfig) - .MatchSnapshot(extension: ".graphql"); - } } diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/RequireTests.cs b/src/HotChocolate/Fusion/test/Composition.Tests/RequireTests.cs new file mode 100644 index 00000000000..2cce2a41ab3 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Composition.Tests/RequireTests.cs @@ -0,0 +1,56 @@ +using CookieCrumble; +using HotChocolate.Fusion.Shared; +using HotChocolate.Skimmed.Serialization; +using Xunit.Abstractions; +using static HotChocolate.Fusion.Shared.DemoProjectSchemaExtensions; + +namespace HotChocolate.Fusion.Composition; + +public class RequireTests +{ + private readonly Func _logFactory; + + public RequireTests(ITestOutputHelper output) + { + _logFactory = () => new TestCompositionLog(output); + } + + [Fact] + public async Task Require_Scalar_Arguments_No_Overloads() + { + // arrange + using var demoProject = await DemoProject.CreateAsync(); + + var composer = new FusionGraphComposer(logFactory: _logFactory); + + var fusionConfig = await composer.ComposeAsync( + new[] + { + demoProject.Accounts.ToConfiguration(), + demoProject.Reviews.ToConfiguration(), + demoProject.Products.ToConfiguration( + """ + extend type Query { + productById(id: ID! @is(field: "id")): Product + } + """), + demoProject.Shipping.ToConfiguration( + """ + extend type Query { + productById(id: ID! @is(field: "id")): Product + } + + extend type Product { + deliveryEstimate( + size: Int! @require(field: "dimension { size }"), + weight: Int! @require(field: "dimension { weight }"), + zip: String!): DeliveryEstimate! + } + """), + }); + + SchemaFormatter + .FormatAsString(fusionConfig) + .MatchSnapshot(extension: ".graphql"); + } +} diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DemoIntegrationTests.Accounts_And_Reviews_Products_Shipping_With_Is_Directive.graphql b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/RequireTests.Require_Scalar_Arguments_No_Overloads.graphql similarity index 100% rename from src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DemoIntegrationTests.Accounts_And_Reviews_Products_Shipping_With_Is_Directive.graphql rename to src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/RequireTests.Require_Scalar_Arguments_No_Overloads.graphql diff --git a/src/HotChocolate/Fusion/test/Shared/DemoProjectSchemaExtensions.cs b/src/HotChocolate/Fusion/test/Shared/DemoProjectSchemaExtensions.cs index 6baa77401b2..5ce00627ab6 100644 --- a/src/HotChocolate/Fusion/test/Shared/DemoProjectSchemaExtensions.cs +++ b/src/HotChocolate/Fusion/test/Shared/DemoProjectSchemaExtensions.cs @@ -50,8 +50,8 @@ extend type Query { extend type Product { deliveryEstimate( - size: Int! @is(field: "dimension { size }"), - weight: Int! @is(field: "dimension { weight }"), + size: Int! @require(field: "dimension { size }"), + weight: Int! @require(field: "dimension { weight }"), zip: String!): DeliveryEstimate! } """; diff --git a/src/HotChocolate/Fusion/test/Shared/Shipping/ProductDimension.cs b/src/HotChocolate/Fusion/test/Shared/Shipping/DeliveryEstimate.cs similarity index 100% rename from src/HotChocolate/Fusion/test/Shared/Shipping/ProductDimension.cs rename to src/HotChocolate/Fusion/test/Shared/Shipping/DeliveryEstimate.cs