diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs index 7ca3fbd7085..95041903ec0 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs @@ -17,7 +17,8 @@ internal sealed class ConnectionType internal ConnectionType( string connectionName, TypeReference nodeType, - bool withTotalCount) + bool includeTotalCount, + bool includeNodesField) { if (nodeType is null) { @@ -38,31 +39,35 @@ internal ConnectionType( TypeContext.Output, factory: _ => new EdgeType(connectionName, nodeType)); - Definition = CreateTypeDefinition(withTotalCount, edgesType); + Definition = CreateTypeDefinition(includeTotalCount, includeNodesField, edgesType); Definition.Name = NameHelper.CreateConnectionName(connectionName); Definition.Dependencies.Add(new(nodeType)); - Definition.Configurations.Add( - new CompleteConfiguration( - (c, d) => - { - var definition = (ObjectTypeDefinition)d; - var nodes = definition.Fields.First(IsNodesField); - nodes.Type = TypeReference.Parse( - $"[{c.GetType(nodeType).Print()}]", - TypeContext.Output); - }, - Definition, - ApplyConfigurationOn.BeforeNaming, - nodeType, - TypeDependencyFulfilled.Named)); Definition.Configurations.Add( new CompleteConfiguration( (c, _) => EdgeType = c.GetType(TypeReference.Create(edgeTypeName)), Definition, ApplyConfigurationOn.BeforeCompletion)); + + if (includeNodesField) + { + Definition.Configurations.Add( + new CompleteConfiguration( + (c, d) => + { + var definition = (ObjectTypeDefinition)d; + var nodes = definition.Fields.First(IsNodesField); + nodes.Type = TypeReference.Parse( + $"[{c.GetType(nodeType).Print()}]", + TypeContext.Output); + }, + Definition, + ApplyConfigurationOn.BeforeNaming, + nodeType, + TypeDependencyFulfilled.Named)); + } } - internal ConnectionType(TypeReference nodeType, bool withTotalCount) + internal ConnectionType(TypeReference nodeType, bool includeTotalCount, bool includeNodesField) { if (nodeType is null) { @@ -78,7 +83,7 @@ internal ConnectionType(TypeReference nodeType, bool withTotalCount) // the property is set later in the configuration ConnectionName = default!; - Definition = CreateTypeDefinition(withTotalCount); + Definition = CreateTypeDefinition(includeTotalCount, includeNodesField); Definition.Dependencies.Add(new(nodeType)); Definition.Dependencies.Add(new(edgeType)); Definition.NeedsNameCompletion = true; @@ -92,16 +97,19 @@ internal ConnectionType(TypeReference nodeType, bool withTotalCount) var definition = (ObjectTypeDefinition)d; var edges = definition.Fields.First(IsEdgesField); - var nodes = definition.Fields.First(IsNodesField); definition.Name = NameHelper.CreateConnectionName(ConnectionName); edges.Type = TypeReference.Parse( $"[{NameHelper.CreateEdgeName(ConnectionName)}!]", TypeContext.Output); - nodes.Type = TypeReference.Parse( - $"[{type.Print()}]", - TypeContext.Output); + if (includeNodesField) + { + var nodes = definition.Fields.First(IsNodesField); + nodes.Type = TypeReference.Parse( + $"[{type.Print()}]", + TypeContext.Output); + } }, Definition, ApplyConfigurationOn.BeforeNaming, @@ -151,7 +159,8 @@ private bool IsOfTypeWithRuntimeType( result is null || RuntimeType.IsInstanceOfType(result); private static ObjectTypeDefinition CreateTypeDefinition( - bool withTotalCount, + bool includeTotalCount, + bool includeNodesField, TypeReference? edgesType = null) { var definition = new ObjectTypeDefinition @@ -173,13 +182,16 @@ private static ObjectTypeDefinition CreateTypeDefinition( pureResolver: GetEdges) { CustomSettings = { ContextDataKeys.Edges } }); - definition.Fields.Add(new( - Names.Nodes, - ConnectionType_Nodes_Description, - pureResolver: GetNodes) - { CustomSettings = { ContextDataKeys.Nodes } }); + if (includeNodesField) + { + definition.Fields.Add(new( + Names.Nodes, + ConnectionType_Nodes_Description, + pureResolver: GetNodes) + { CustomSettings = { ContextDataKeys.Nodes } }); + } - if (withTotalCount) + if (includeTotalCount) { definition.Fields.Add(new( Names.TotalCount, diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs index b7abe20dc82..ce05ee31b87 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -358,7 +358,8 @@ private static TypeReference CreateConnectionTypeRef( return CreateConnectionType( connectionName, nodeType, - options.IncludeTotalCount ?? false); + options.IncludeTotalCount ?? false, + options.IncludeNodesField ?? IncludeNodesField); } private static CursorPagingProvider ResolvePagingProvider( @@ -420,13 +421,14 @@ providerName is null private static TypeReference CreateConnectionType( string? connectionName, TypeReference nodeType, - bool withTotalCount) + bool includeTotalCount, + bool includeNodesField) { return connectionName is null ? TypeReference.Create( "HotChocolate_Types_Connection", nodeType, - _ => new ConnectionType(nodeType, withTotalCount), + _ => new ConnectionType(nodeType, includeTotalCount, includeNodesField), TypeContext.Output) : TypeReference.Create( connectionName + "Connection", @@ -434,7 +436,8 @@ private static TypeReference CreateConnectionType( factory: _ => new ConnectionType( connectionName, nodeType, - withTotalCount)); + includeTotalCount, + includeNodesField)); } private static string EnsureConnectionNameCasing(string connectionName) diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingDefaults.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingDefaults.cs index c5a08f1fa2b..c04c2f06ea1 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingDefaults.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingDefaults.cs @@ -15,4 +15,6 @@ public static class PagingDefaults public const bool InferCollectionSegmentNameFromField = true; public const bool RequirePagingBoundaries = false; + + public const bool IncludeNodesField = true; } diff --git a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs index 9d65392f573..ca987a9991a 100644 --- a/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs +++ b/src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs @@ -18,8 +18,9 @@ public class PagingOptions public int? MaxPageSize { get; set; } /// - /// Defines if the total count of the paged data set - /// shall be included into the paging result type. + /// Defines whether a totalCount field shall be + /// exposed on the Connection type, returning the total + /// count of items in the paginated data set. /// public bool? IncludeTotalCount { get; set; } @@ -51,6 +52,13 @@ public class PagingOptions /// public string? ProviderName { get; set; } + /// + /// Defines whether a nodes field shall be + /// exposed on the Connection type, returning the + /// flattened nodes of the edges field. + /// + public bool? IncludeNodesField { get; set; } + /// /// Merges the options into this options instance wherever /// a property is not set. @@ -68,6 +76,7 @@ internal void Merge(PagingOptions other) InferConnectionNameFromField ??= other.InferConnectionNameFromField; InferCollectionSegmentNameFromField ??= other.InferCollectionSegmentNameFromField; ProviderName ??= other.ProviderName; + IncludeNodesField ??= other.IncludeNodesField; } /// @@ -83,6 +92,7 @@ internal PagingOptions Copy() RequirePagingBoundaries = RequirePagingBoundaries, InferConnectionNameFromField = InferConnectionNameFromField, InferCollectionSegmentNameFromField = InferCollectionSegmentNameFromField, - ProviderName = ProviderName + ProviderName = ProviderName, + IncludeNodesField = IncludeNodesField }; } diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs index 2fc4e4e6d33..41ab9f96b31 100644 --- a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs @@ -23,6 +23,21 @@ public async Task Simple_StringList_Schema() executor.Schema.Print().MatchSnapshot(); } + [Fact] + public async Task IncludeNodesField_False() + { + var executor = + await new ServiceCollection() + .AddGraphQL() + .AddQueryType() + .ModifyPagingOptions(o => o.IncludeNodesField = false) + .Services + .BuildServiceProvider() + .GetRequestExecutorAsync(); + + executor.Schema.Print().MatchSnapshot(); + } + [Fact] public async Task SetPagingOptionsIsStillApplied() { diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.IncludeNodesField_False.snap b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.IncludeNodesField_False.snap new file mode 100644 index 00000000000..e218cfdafd5 --- /dev/null +++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.IncludeNodesField_False.snap @@ -0,0 +1,75 @@ +schema { + query: Query +} + +"A connection to a list of items." +type ExplicitTypeConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [ExplicitTypeEdge!] +} + +"An edge in a connection." +type ExplicitTypeEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: String! +} + +type Foo { + bar: String! +} + +"A connection to a list of items." +type LettersConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [LettersEdge!] +} + +"An edge in a connection." +type LettersEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: String! +} + +"A connection to a list of items." +type NestedObjectListConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [NestedObjectListEdge!] + "Identifies the total count of items in the connection." + totalCount: Int! +} + +"An edge in a connection." +type NestedObjectListEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: [Foo!]! +} + +"Information about pagination in a connection." +type PageInfo { + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String + "When paginating forwards, the cursor to continue." + endCursor: String +} + +type Query { + letters("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): LettersConnection + explicitType("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): ExplicitTypeConnection + nestedObjectList("Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String): NestedObjectListConnection +}