diff --git a/src/HotChocolate/Utilities/src/Utilities.Introspection/CapabilityInspector.cs b/src/HotChocolate/Utilities/src/Utilities.Introspection/CapabilityInspector.cs index 44641f0bc60..b2191920693 100644 --- a/src/HotChocolate/Utilities/src/Utilities.Introspection/CapabilityInspector.cs +++ b/src/HotChocolate/Utilities/src/Utilities.Introspection/CapabilityInspector.cs @@ -118,6 +118,9 @@ private async Task InspectDirectiveTypeAsync() }, { "name": "isRepeatable" # <--- and we are looking for this! + }, + { + "name": "isDeprecated" # <--- and for this! } ] } @@ -145,6 +148,7 @@ private async Task InspectDirectiveTypeAsync() { var locations = false; var isRepeatable = false; + var isDeprecated = false; foreach (var field in fields.EnumerateArray()) { @@ -168,7 +172,13 @@ private async Task InspectDirectiveTypeAsync() _features.HasRepeatableDirectives = true; } - if (locations && isRepeatable) + if (fieldNameString.EqualsOrdinal("isDeprecated")) + { + isDeprecated = true; + _features.HasDirectiveDeprecation = true; + } + + if (locations && isRepeatable && isDeprecated) { return; } diff --git a/src/HotChocolate/Utilities/src/Utilities.Introspection/IntrospectionFormatter.cs b/src/HotChocolate/Utilities/src/Utilities.Introspection/IntrospectionFormatter.cs index 15c51a3ebd2..7f757dd3197 100644 --- a/src/HotChocolate/Utilities/src/Utilities.Introspection/IntrospectionFormatter.cs +++ b/src/HotChocolate/Utilities/src/Utilities.Introspection/IntrospectionFormatter.cs @@ -267,7 +267,9 @@ private static DirectiveDefinitionNode CreateDirectiveDefinition( CreateDescription(directive.Description), directive.IsRepeatable ?? false, CreateInputValues(directive.Args), - [], + CreateDeprecatedDirective( + directive.IsDeprecated, + directive.DeprecationReason), locations ); } diff --git a/src/HotChocolate/Utilities/src/Utilities.Introspection/IntrospectionQueryBuilder.cs b/src/HotChocolate/Utilities/src/Utilities.Introspection/IntrospectionQueryBuilder.cs index a7f7249c4a4..9576024d237 100644 --- a/src/HotChocolate/Utilities/src/Utilities.Introspection/IntrospectionQueryBuilder.cs +++ b/src/HotChocolate/Utilities/src/Utilities.Introspection/IntrospectionQueryBuilder.cs @@ -57,10 +57,7 @@ public static DocumentNode Build(ServerCapabilities features, IntrospectionOptio selections.Add(CreateTypesField()); - selections.Add( - CreateDirectivesField( - features.HasDirectiveLocations, - features.HasRepeatableDirectives)); + selections.Add(CreateDirectivesField(features)); return new DocumentNode( new IDefinitionNode[] @@ -103,28 +100,16 @@ private static FieldNode CreateTypesField() Array.Empty()) })); - private static FieldNode CreateDirectivesField(bool hasLocationsField, bool hasRepeatableDirective) + private static FieldNode CreateDirectivesField(ServerCapabilities features) { var selections = new List { new FieldNode("name"), new FieldNode("description"), - new FieldNode( - new NameNode("args"), - null, - Array.Empty(), - Array.Empty(), - new SelectionSetNode( - new ISelectionNode[] - { - new FragmentSpreadNode( - null, - new NameNode("InputValue"), - Array.Empty()) - })) + CreateArgsField(features.HasArgumentDeprecation) }; - if (hasLocationsField) + if (features.HasDirectiveLocations) { selections.Add(new FieldNode("locations")); } @@ -135,16 +120,24 @@ private static FieldNode CreateDirectivesField(bool hasLocationsField, bool hasR selections.Add(new FieldNode("onField")); } - if (hasRepeatableDirective) + if (features.HasRepeatableDirectives) { selections.Add(new FieldNode("isRepeatable")); } + if (features.HasDirectiveDeprecation) + { + selections.Add(new FieldNode("isDeprecated")); + selections.Add(new FieldNode("deprecationReason")); + } + return new FieldNode( new NameNode("directives"), null, Array.Empty(), - Array.Empty(), + features.HasDirectiveDeprecation + ? [new ArgumentNode("includeDeprecated", true)] + : Array.Empty(), new SelectionSetNode(selections)); } diff --git a/src/HotChocolate/Utilities/src/Utilities.Introspection/Models/Directive.cs b/src/HotChocolate/Utilities/src/Utilities.Introspection/Models/Directive.cs index 8c71977ecf0..2406d2842c2 100644 --- a/src/HotChocolate/Utilities/src/Utilities.Introspection/Models/Directive.cs +++ b/src/HotChocolate/Utilities/src/Utilities.Introspection/Models/Directive.cs @@ -9,6 +9,8 @@ internal class Directive public ICollection Args { get; set; } public ICollection Locations { get; set; } public bool? IsRepeatable { get; set; } + public bool IsDeprecated { get; set; } + public string DeprecationReason { get; set; } public bool OnOperation { get; set; } public bool OnFragment { get; set; } public bool OnField { get; set; } diff --git a/src/HotChocolate/Utilities/src/Utilities.Introspection/ServerCapabilities.cs b/src/HotChocolate/Utilities/src/Utilities.Introspection/ServerCapabilities.cs index 07829f1fb9c..737cbf2c439 100644 --- a/src/HotChocolate/Utilities/src/Utilities.Introspection/ServerCapabilities.cs +++ b/src/HotChocolate/Utilities/src/Utilities.Introspection/ServerCapabilities.cs @@ -36,6 +36,11 @@ public class ServerCapabilities /// public bool HasArgumentDeprecation { get; internal set; } + /// + /// Gets a value that defines if the GraphQL server supports directive definition deprecation. + /// + public bool HasDirectiveDeprecation { get; internal set; } + /// /// Gets a value that defines if the GraphQL server supports schema descriptions. /// diff --git a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionClientTests.cs b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionClientTests.cs index 2e6c2b0af66..6da47e2fddb 100644 --- a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionClientTests.cs +++ b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionClientTests.cs @@ -1,5 +1,9 @@ using System.Net; using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.Language; +using HotChocolate.Language.Utilities; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; // ReSharper disable AccessToDisposedClosure @@ -19,13 +23,19 @@ public async Task InspectServer() var features = await IntrospectionClient.InspectServerAsync(client, TestContext.Current.CancellationToken); // assert - Assert.True(features.HasArgumentDeprecation); - Assert.True(features.HasDirectiveLocations); - Assert.True(features.HasSubscriptionSupport); - Assert.True(features.HasSchemaDescription); - Assert.True(features.HasRepeatableDirectives); - Assert.True(features.HasDeferSupport); - Assert.True(features.HasStreamSupport); + features.MatchInlineSnapshot( + """ + { + "HasDirectiveLocations": true, + "HasRepeatableDirectives": true, + "HasSubscriptionSupport": true, + "HasDeferSupport": true, + "HasStreamSupport": true, + "HasArgumentDeprecation": true, + "HasDirectiveDeprecation": true, + "HasSchemaDescription": true + } + """); } [Fact] @@ -54,6 +64,32 @@ public async Task IntrospectServer() schema.ToString(true).MatchSnapshot(); } + [Fact] + public async Task IntrospectServer_With_DeprecatedDirective() + { + // arrange + var server = CreateStarWarsServer( + configureServices: services => services + .AddGraphQL() + .AddDirectiveType()); + var client = server.CreateClient(); + client.BaseAddress = new Uri("http://localhost:5000/graphql"); + + // act + var schema = await IntrospectionClient.IntrospectServerAsync(client, TestContext.Current.CancellationToken); + + // assert + var directive = Assert.Single( + schema.Definitions.OfType(), + t => t.Name.Value.Equals("obsolete", StringComparison.Ordinal)); + directive.Print(indented: true).MatchInlineSnapshot( + """ + directive @obsolete( + obsoleteArg: String @deprecated(reason: "Argument no longer supported.") + ) @deprecated(reason: "Directive no longer supported.") on FIELD + """); + } + [Fact] public async Task IntrospectServer_HttpClient_Is_Null() { @@ -110,6 +146,22 @@ public async Task IntrospectServer_Transport_Error() Assert.Equal("Something went wrong", exception.Message); } + private sealed class ObsoleteDirectiveType : DirectiveType + { + protected override void Configure(IDirectiveTypeDescriptor descriptor) + { + descriptor + .Name("obsolete") + .Location(Types.DirectiveLocation.Field) + .Deprecated("Directive no longer supported."); + + descriptor + .Argument("obsoleteArg") + .Type() + .Deprecated("Argument no longer supported."); + } + } + private class CustomHttpClientHandler(HttpStatusCode? httpStatusCode = null) : HttpClientHandler { protected override Task SendAsync( diff --git a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionFormatterTests.cs b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionFormatterTests.cs index 3b27d1623df..f59cdfcc6e9 100644 --- a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionFormatterTests.cs +++ b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionFormatterTests.cs @@ -33,6 +33,36 @@ public void DeserializeIntrospectionWithIntDefaultValues() schema.ToString(true).MatchSnapshot(); } + [Fact] + public void DeserializeIntrospectionWithDeprecatedDirectives() + { + // arrange + var json = FileResource.Open("IntrospectionWithDeprecatedDirectives.json"); + var result = JsonSerializer.Deserialize(json, SerializerOptions); + + // act + var schema = IntrospectionFormatter.Format(result!); + + // assert + schema.ToString(true).MatchInlineSnapshot( + """ + schema { + query: Query + } + + type Query { + field: String + } + + "Built-in String" + scalar String + + directive @obsolete( + obsoleteArg: String @deprecated(reason: "Argument no longer supported.") + ) @deprecated(reason: "Directive no longer supported.") on FIELD + """); + } + [Fact] public void DeserializeIntrospectionWithNullDeprecationReason() { diff --git a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionQueryBuilderTests.cs b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionQueryBuilderTests.cs index 33c015c0dae..fabbb29fc81 100644 --- a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionQueryBuilderTests.cs +++ b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/IntrospectionQueryBuilderTests.cs @@ -35,6 +35,23 @@ public void Create_Query_With_ArgumentDeprecation() document.Print().MatchSnapshot(); } + [Fact] + public void Create_Query_With_DirectiveDeprecation() + { + // arrange + var options = new IntrospectionOptions(); + var features = new ServerCapabilities + { + HasDirectiveDeprecation = true + }; + + // act + var document = IntrospectionQueryBuilder.Build(features, options); + + //assert + document.Print().MatchSnapshot(); + } + [Fact] public void Create_Query_With_DirectiveLocations() { diff --git a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__resources__/IntrospectionWithDeprecatedDirectives.json b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__resources__/IntrospectionWithDeprecatedDirectives.json new file mode 100644 index 00000000000..b5ce2003552 --- /dev/null +++ b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__resources__/IntrospectionWithDeprecatedDirectives.json @@ -0,0 +1,66 @@ +{ + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": null, + "subscriptionType": null, + "types": [{ + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [{ + "name": "field", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "Built-in String", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + } + ], + "directives": [{ + "name": "obsolete", + "description": null, + "locations": ["FIELD"], + "args": [{ + "name": "obsoleteArg", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": true, + "deprecationReason": "Argument no longer supported." + } + ], + "isRepeatable": false, + "isDeprecated": true, + "deprecationReason": "Directive no longer supported." + } + ] + } + } +} diff --git a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionQueryBuilderTests.Create_Query_With_ArgumentDeprecation.snap b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionQueryBuilderTests.Create_Query_With_ArgumentDeprecation.snap index b20d4d431e5..4fb412cc9cb 100644 --- a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionQueryBuilderTests.Create_Query_With_ArgumentDeprecation.snap +++ b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionQueryBuilderTests.Create_Query_With_ArgumentDeprecation.snap @@ -12,8 +12,10 @@ query IntrospectionQuery { directives { name description - args { + args(includeDeprecated: true) { ...InputValue + isDeprecated + deprecationReason } onOperation onFragment diff --git a/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionQueryBuilderTests.Create_Query_With_DirectiveDeprecation.snap b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionQueryBuilderTests.Create_Query_With_DirectiveDeprecation.snap new file mode 100644 index 00000000000..bef392d8686 --- /dev/null +++ b/src/HotChocolate/Utilities/test/Utilities.Introspection.Tests/__snapshots__/IntrospectionQueryBuilderTests.Create_Query_With_DirectiveDeprecation.snap @@ -0,0 +1,92 @@ +query IntrospectionQuery { + __schema { + queryType { + name + } + mutationType { + name + } + types { + ...FullType + } + directives(includeDeprecated: true) { + name + description + args { + ...InputValue + } + onOperation + onFragment + onField + isDeprecated + deprecationReason + } + } +} + +fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} + +fragment InputValue on __InputValue { + name + description + type { + ...TypeRef + } + defaultValue +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } +}