diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/DeprecatedDirective.cs b/src/HotChocolate/Core/src/Types/Types/Directives/DeprecatedDirective.cs index a76fd676c9c..c0c60eb4975 100644 --- a/src/HotChocolate/Core/src/Types/Types/Directives/DeprecatedDirective.cs +++ b/src/HotChocolate/Core/src/Types/Types/Directives/DeprecatedDirective.cs @@ -72,11 +72,11 @@ public static DirectiveNode CreateNode(string? reason = null) var arguments = reason is null ? Array.Empty() - : new[] { new ArgumentNode(DeprecatedDirectiveType.Names.Reason, reason) }; + : new[] { new ArgumentNode(WellKnownDirectives.DeprecationReasonArgument, reason) }; return new DirectiveNode( null, - new NameNode(DeprecatedDirectiveType.Names.Deprecated), + new NameNode(WellKnownDirectives.Deprecated), arguments); } } diff --git a/src/HotChocolate/Core/src/Types/Types/Directives/DeprecatedDirectiveType.cs b/src/HotChocolate/Core/src/Types/Types/Directives/DeprecatedDirectiveType.cs index e0770b15ea4..4bf1ea551a5 100644 --- a/src/HotChocolate/Core/src/Types/Types/Directives/DeprecatedDirectiveType.cs +++ b/src/HotChocolate/Core/src/Types/Types/Directives/DeprecatedDirectiveType.cs @@ -16,7 +16,7 @@ protected override void Configure( IDirectiveTypeDescriptor descriptor) { descriptor - .Name(Names.Deprecated) + .Name(WellKnownDirectives.Deprecated) .Description(TypeResources.DeprecatedDirectiveType_TypeDescription) .Location(DirectiveLocation.FieldDefinition) .Location(DirectiveLocation.ArgumentDefinition) @@ -25,15 +25,9 @@ protected override void Configure( descriptor .Argument(t => t.Reason) - .Name(Names.Reason) + .Name(WellKnownDirectives.DeprecationReasonArgument) .Description(TypeResources.DeprecatedDirectiveType_ReasonDescription) .Type() .DefaultValue(WellKnownDirectives.DeprecationDefaultReason); } - - public static class Names - { - public const string Deprecated = "deprecated"; - public const string Reason = "reason"; - } } diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/DeprecationMergeTests.cs b/src/HotChocolate/Fusion/test/Composition.Tests/DeprecationMergeTests.cs new file mode 100644 index 00000000000..ae110337a64 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Composition.Tests/DeprecationMergeTests.cs @@ -0,0 +1,136 @@ +using Xunit.Abstractions; + +namespace HotChocolate.Fusion.Composition; + +public class DeprecationMergeTests(ITestOutputHelper output) : CompositionTestBase(output) +{ + [Fact] + public async Task Merge_Entity_Output_Field_Deprecation() + => await Succeed( + """ + type Query { + brandById(id: ID!): Brand + } + + type Brand implements Node { + id: ID! + name: String! @deprecated(reason: "Some reason") + } + + interface Node { + id: ID! + } + """, + """ + type Query { + brandById(id: ID!): Brand + } + + type Brand implements Node { + id: ID! + newName: String! + } + + interface Node { + id: ID! + } + """); + + [Fact] + public async Task Merge_Entity_Output_Field_Argument_Deprecation() + => await Succeed( + """ + type Query { + brandById(id: ID!): Brand + } + + type Brand implements Node { + id: ID! + name(includeFirstName: Boolean @deprecated(reason: "Some reason")): String! + } + + interface Node { + id: ID! + } + """, + """ + type Query { + brandById(id: ID!): Brand + } + + type Brand implements Node { + id: ID! + name(includeFirstName: Boolean): String! + } + + interface Node { + id: ID! + } + """); + + [Fact] + public async Task Merge_Output_Field_Deprecation() + => await Succeed( + """ + type Query { + brand: Brand + } + + type Brand { + name: String! @deprecated(reason: "Some reason") + } + """, + """ + type Query { + brand: Brand + } + + type Brand { + newName: String! + } + """); + + [Fact] + public async Task Merge_Output_Field_Argument_Deprecation() + => await Succeed( + """ + type Query { + brand: Brand + } + + type Brand { + name(includeFirstName: Boolean @deprecated(reason: "Some reason")): String! + } + """, + """ + type Query { + brand: Brand + } + + type Brand { + name(includeFirstName: Boolean): String! + } + """); + + [Fact] + public async Task Merge_Enum_Value_Deprecation() + => await Succeed( + """ + type Query { + value: OrderStatus + } + + enum OrderStatus { + SENT_OUT @deprecated(reason: "Some reason") + } + """, + """ + type Query { + value: OrderStatus + } + + enum OrderStatus { + SHIPPED + } + """); +} diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Entity_Output_Field_Argument_Deprecation.graphql b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Entity_Output_Field_Argument_Deprecation.graphql new file mode 100644 index 00000000000..cd91ddf7af7 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Entity_Output_Field_Argument_Deprecation.graphql @@ -0,0 +1,34 @@ +schema + @fusion(version: 1) + @transport(subgraph: "A", location: "https:\/\/localhost:5001\/graphql", kind: "HTTP") + @transport(subgraph: "B", location: "https:\/\/localhost:5002\/graphql", kind: "HTTP") { + query: Query +} + +type Query { + brandById(id: ID!): Brand + @variable(subgraph: "A", name: "id", argument: "id") + @resolver(subgraph: "A", select: "{ brandById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + @variable(subgraph: "B", name: "id", argument: "id") + @resolver(subgraph: "B", select: "{ brandById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) +} + +type Brand implements Node + @variable(subgraph: "A", name: "Brand_id", select: "id") + @variable(subgraph: "B", name: "Brand_id", select: "id") + @resolver(subgraph: "A", select: "{ brandById(id: $Brand_id) }", arguments: [ { name: "Brand_id", type: "ID!" } ]) + @resolver(subgraph: "B", select: "{ brandById(id: $Brand_id) }", arguments: [ { name: "Brand_id", type: "ID!" } ]) { + id: ID! + @source(subgraph: "A") + @source(subgraph: "B") + name(includeFirstName: Boolean + @deprecated(reason: "Some reason")): String! + @source(subgraph: "A") + @variable(subgraph: "A", name: "includeFirstName", argument: "includeFirstName") + @source(subgraph: "B") + @variable(subgraph: "B", name: "includeFirstName", argument: "includeFirstName") +} + +interface Node { + id: ID! +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Entity_Output_Field_Deprecation.graphql b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Entity_Output_Field_Deprecation.graphql new file mode 100644 index 00000000000..7d21b665113 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Entity_Output_Field_Deprecation.graphql @@ -0,0 +1,33 @@ +schema + @fusion(version: 1) + @transport(subgraph: "A", location: "https:\/\/localhost:5001\/graphql", kind: "HTTP") + @transport(subgraph: "B", location: "https:\/\/localhost:5002\/graphql", kind: "HTTP") { + query: Query +} + +type Query { + brandById(id: ID!): Brand + @variable(subgraph: "A", name: "id", argument: "id") + @resolver(subgraph: "A", select: "{ brandById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) + @variable(subgraph: "B", name: "id", argument: "id") + @resolver(subgraph: "B", select: "{ brandById(id: $id) }", arguments: [ { name: "id", type: "ID!" } ]) +} + +type Brand implements Node + @variable(subgraph: "A", name: "Brand_id", select: "id") + @variable(subgraph: "B", name: "Brand_id", select: "id") + @resolver(subgraph: "A", select: "{ brandById(id: $Brand_id) }", arguments: [ { name: "Brand_id", type: "ID!" } ]) + @resolver(subgraph: "B", select: "{ brandById(id: $Brand_id) }", arguments: [ { name: "Brand_id", type: "ID!" } ]) { + id: ID! + @source(subgraph: "A") + @source(subgraph: "B") + name: String! + @source(subgraph: "A") + @deprecated(reason: "Some reason") + newName: String! + @source(subgraph: "B") +} + +interface Node { + id: ID! +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Enum_Value_Deprecation.graphql b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Enum_Value_Deprecation.graphql new file mode 100644 index 00000000000..2eb1aef33dc --- /dev/null +++ b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Enum_Value_Deprecation.graphql @@ -0,0 +1,20 @@ +schema + @fusion(version: 1) + @transport(subgraph: "A", location: "https:\/\/localhost:5001\/graphql", kind: "HTTP") + @transport(subgraph: "B", location: "https:\/\/localhost:5002\/graphql", kind: "HTTP") { + query: Query +} + +type Query { + value: OrderStatus + @resolver(subgraph: "A", select: "{ value }") + @resolver(subgraph: "B", select: "{ value }") +} + +enum OrderStatus { + SENT_OUT + @source(subgraph: "A") + @deprecated(reason: "Some reason") + SHIPPED + @source(subgraph: "B") +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Output_Field_Argument_Deprecation.graphql b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Output_Field_Argument_Deprecation.graphql new file mode 100644 index 00000000000..f0f33899875 --- /dev/null +++ b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Output_Field_Argument_Deprecation.graphql @@ -0,0 +1,21 @@ +schema + @fusion(version: 1) + @transport(subgraph: "A", location: "https:\/\/localhost:5001\/graphql", kind: "HTTP") + @transport(subgraph: "B", location: "https:\/\/localhost:5002\/graphql", kind: "HTTP") { + query: Query +} + +type Query { + brand: Brand + @resolver(subgraph: "A", select: "{ brand }") + @resolver(subgraph: "B", select: "{ brand }") +} + +type Brand { + name(includeFirstName: Boolean + @deprecated(reason: "Some reason")): String! + @source(subgraph: "A") + @variable(subgraph: "A", name: "includeFirstName", argument: "includeFirstName") + @source(subgraph: "B") + @variable(subgraph: "B", name: "includeFirstName", argument: "includeFirstName") +} \ No newline at end of file diff --git a/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Output_Field_Deprecation.graphql b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Output_Field_Deprecation.graphql new file mode 100644 index 00000000000..449081c98de --- /dev/null +++ b/src/HotChocolate/Fusion/test/Composition.Tests/__snapshots__/DeprecationMergeTests.Merge_Output_Field_Deprecation.graphql @@ -0,0 +1,20 @@ +schema + @fusion(version: 1) + @transport(subgraph: "A", location: "https:\/\/localhost:5001\/graphql", kind: "HTTP") + @transport(subgraph: "B", location: "https:\/\/localhost:5002\/graphql", kind: "HTTP") { + query: Query +} + +type Query { + brand: Brand + @resolver(subgraph: "A", select: "{ brand }") + @resolver(subgraph: "B", select: "{ brand }") +} + +type Brand { + name: String! + @source(subgraph: "A") + @deprecated(reason: "Some reason") + newName: String! + @source(subgraph: "B") +} \ No newline at end of file diff --git a/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaFormatter.cs b/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaFormatter.cs index 65bbefa2ba7..251a05031c3 100644 --- a/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaFormatter.cs +++ b/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaFormatter.cs @@ -1,5 +1,6 @@ using HotChocolate.Language; using HotChocolate.Language.Utilities; +using HotChocolate.Utilities; namespace HotChocolate.Skimmed.Serialization; @@ -9,10 +10,10 @@ public static class SchemaFormatter private static readonly SyntaxSerializerOptions _options = new() { - Indented = true, + Indented = true, MaxDirectivesPerLine = 0 }; - + public static string FormatAsString(Schema schema, bool indented = true) { var context = new VisitorContext(); @@ -20,9 +21,9 @@ public static string FormatAsString(Schema schema, bool indented = true) if (!indented) { - ((DocumentNode) context.Result!).ToString(false); + ((DocumentNode)context.Result!).ToString(false); } - + return ((DocumentNode)context.Result!).ToString(_options); } @@ -119,7 +120,7 @@ public override void VisitTypes(TypeCollection types, VisitorContext context) foreach (var type in types.OfType().OrderBy(t => t.Name)) { - if(context.Schema?.QueryType == type || + if (context.Schema?.QueryType == type || context.Schema?.MutationType == type || context.Schema?.SubscriptionType == type) { @@ -156,7 +157,7 @@ public override void VisitTypes(TypeCollection types, VisitorContext context) foreach (var type in types.OfType().OrderBy(t => t.Name)) { - if (type is { IsSpecScalar: true } || SpecScalarTypes.IsSpecScalar(type.Name)) + if (type is { IsSpecScalar: true } || SpecScalarTypes.IsSpecScalar(type.Name)) { type.IsSpecScalar = true; continue; @@ -326,6 +327,8 @@ public override void VisitEnumValue(EnumValue value, VisitorContext context) VisitDirectives(value.Directives, context); var directives = (List)context.Result!; + directives = ApplyDeprecatedDirective(value, directives); + context.Result = new EnumValueDefinitionNode( null, new NameNode(value.Name), @@ -399,6 +402,8 @@ public override void VisitOutputField(OutputField field, VisitorContext context) VisitDirectives(field.Directives, context); var directives = (List)context.Result!; + directives = ApplyDeprecatedDirective(field, directives); + context.Result = new FieldDefinitionNode( null, new NameNode(field.Name), @@ -428,6 +433,9 @@ public override void VisitInputFields( public override void VisitInputField(InputField field, VisitorContext context) { VisitDirectives(field.Directives, context); + var directives = (List)context.Result!; + + directives = ApplyDeprecatedDirective(field, directives); context.Result = new InputValueDefinitionNode( null, @@ -437,7 +445,7 @@ field.Description is not null : null, field.Type.ToTypeNode(), field.DefaultValue, - (List)context.Result!); + directives); } public override void VisitDirectives(DirectiveCollection directives, VisitorContext context) @@ -481,6 +489,46 @@ public override void VisitArgument(Argument argument, VisitorContext context) { context.Result = new ArgumentNode(argument.Name, argument.Value); } + + private static List ApplyDeprecatedDirective( + ICanBeDeprecated canBeDeprecated, + List directives) + { + if (canBeDeprecated.IsDeprecated) + { + var deprecateDirective = CreateDeprecatedDirective(canBeDeprecated.DeprecationReason); + + if (directives.Count == 0) + { + directives = new List { deprecateDirective }; + } + else + { + var temp = directives.ToList(); + temp.Add(deprecateDirective); + directives = temp; + } + } + + return directives; + } + + private static DirectiveNode CreateDeprecatedDirective(string? reason = null) + { + if (WellKnownDirectives.DeprecationDefaultReason.EqualsOrdinal(reason)) + { + reason = null; + } + + var arguments = reason is null + ? Array.Empty() + : new[] { new ArgumentNode(WellKnownDirectives.DeprecationReasonArgument, reason) }; + + return new DirectiveNode( + null, + new NameNode(WellKnownDirectives.Deprecated), + arguments); + } } private sealed class VisitorContext