diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs index b43a5b00b66..899d9258a77 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/ConnectionType.cs @@ -20,22 +20,23 @@ internal ConnectionType( string connectionName, TypeReference nodeType, bool includeTotalCount, - bool includeNodesField) + bool includeNodesField, + INamingConventions namingConventions) { ArgumentException.ThrowIfNullOrEmpty(connectionName); ArgumentNullException.ThrowIfNull(nodeType); ConnectionName = connectionName; - var edgeTypeName = NameHelper.CreateEdgeName(connectionName); + var edgeTypeName = NameHelper.CreateEdgeName(namingConventions, connectionName); var edgesType = TypeReference.Parse( $"[{edgeTypeName}!]", TypeContext.Output, - factory: _ => new EdgeType(connectionName, nodeType)); + factory: c => new EdgeType(connectionName, nodeType, c.Naming)); Configuration = CreateConfiguration(includeTotalCount, includeNodesField, edgesType); - Configuration.Name = NameHelper.CreateConnectionName(connectionName); + Configuration.Name = NameHelper.CreateConnectionName(namingConventions, connectionName); Configuration.Dependencies.Add(new TypeDependency(nodeType)); Configuration.Tasks.Add( new OnCompleteTypeSystemConfigurationTask( @@ -70,7 +71,7 @@ internal ConnectionType(TypeReference nodeType, bool includeTotalCount, bool inc TypeReference.Create( ContextDataKeys.EdgeType, nodeType, - _ => new EdgeType(nodeType), + c => new EdgeType(nodeType, c.Naming), TypeContext.Output); // the property is set later in the configuration @@ -90,9 +91,9 @@ internal ConnectionType(TypeReference nodeType, bool includeTotalCount, bool inc var definition = (ObjectTypeConfiguration)d; var edges = definition.Fields.First(IsEdgesField); - definition.Name = NameHelper.CreateConnectionName(ConnectionName); + definition.Name = NameHelper.CreateConnectionName(c.DescriptorContext.Naming, ConnectionName); edges.Type = TypeReference.Parse( - $"[{NameHelper.CreateEdgeName(ConnectionName)}!]", + $"[{NameHelper.CreateEdgeName(c.DescriptorContext.Naming, ConnectionName)}!]", TypeContext.Output); if (includeNodesField) @@ -130,11 +131,13 @@ protected override void OnBeforeRegisterDependencies( ITypeDiscoveryContext context, TypeSystemConfiguration configuration) { - context.Dependencies.Add(new TypeDependency(context.TypeInspector.GetOutputTypeRef(typeof(PageInfoType)))); + context.Dependencies.Add(new TypeDependency( + context.TypeInspector.GetOutputTypeRef(typeof(PageInfoType)))); if (context.DescriptorContext.Options.ApplyShareableToConnections) { - context.Dependencies.Add(new TypeDependency(context.TypeInspector.GetOutputTypeRef(typeof(Shareable)))); + context.Dependencies.Add(new TypeDependency( + context.TypeInspector.GetOutputTypeRef(typeof(Shareable)))); var config = (ObjectTypeConfiguration)configuration; config.AddDirective(Shareable.Instance, context.TypeInspector); @@ -184,25 +187,27 @@ private static ObjectTypeConfiguration CreateConfiguration( if (includeNodesField) { - definition.Fields.Add(new ObjectFieldConfiguration( - Names.Nodes, - ConnectionType_Nodes_Description, - pureResolver: GetNodes) - { - Flags = CoreFieldFlags.ConnectionNodesField - }); + definition.Fields.Add( + new ObjectFieldConfiguration( + Names.Nodes, + ConnectionType_Nodes_Description, + pureResolver: GetNodes) + { + Flags = CoreFieldFlags.ConnectionNodesField + }); } if (includeTotalCount) { - definition.Fields.Add(new ObjectFieldConfiguration( - Names.TotalCount, - ConnectionType_TotalCount_Description, - type: TypeReference.Parse($"{ScalarNames.Int}!"), - pureResolver: GetTotalCount) - { - Flags = CoreFieldFlags.TotalCount - }); + definition.Fields.Add( + new ObjectFieldConfiguration( + Names.TotalCount, + ConnectionType_TotalCount_Description, + type: TypeReference.Parse($"{ScalarNames.Int}!"), + pureResolver: GetTotalCount) + { + Flags = CoreFieldFlags.TotalCount + }); } return definition; diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/EdgeType.cs b/src/HotChocolate/Core/src/Types.CursorPagination/EdgeType.cs index eea10cdbc21..ea9fc3a5153 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/EdgeType.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/EdgeType.cs @@ -15,14 +15,15 @@ internal sealed class EdgeType : ObjectType, IEdgeType { internal EdgeType( string connectionName, - TypeReference nodeType) + TypeReference nodeType, + INamingConventions namingConventions) { ArgumentException.ThrowIfNullOrEmpty(connectionName); ArgumentNullException.ThrowIfNull(nodeType); ConnectionName = connectionName; Configuration = CreateConfiguration(nodeType); - Configuration.Name = NameHelper.CreateEdgeName(connectionName); + Configuration.Name = NameHelper.CreateEdgeName(namingConventions, connectionName); Configuration.Tasks.Add( new OnCompleteTypeSystemConfigurationTask( (c, _) => NodeType = c.GetType(nodeType), @@ -30,7 +31,7 @@ internal EdgeType( ApplyConfigurationOn.BeforeCompletion)); } - internal EdgeType(TypeReference nodeType) + internal EdgeType(TypeReference nodeType, INamingConventions namingConventions) { ArgumentNullException.ThrowIfNull(nodeType); @@ -43,7 +44,7 @@ internal EdgeType(TypeReference nodeType) { var type = c.GetType(nodeType); ConnectionName = type.NamedType().Name; - ((ObjectTypeConfiguration)d).Name = NameHelper.CreateEdgeName(ConnectionName); + ((ObjectTypeConfiguration)d).Name = NameHelper.CreateEdgeName(namingConventions, ConnectionName); }, Configuration, ApplyConfigurationOn.BeforeNaming, diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs index c387e235765..cda94497fc0 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -362,6 +362,7 @@ private static TypeReference CreateConnectionTypeRef( // last but not least we create a type reference that can be put on the field definition // to tell the type discovery that this field needs this result type. return CreateConnectionType( + context, connectionName, nodeType, options.IncludeTotalCount ?? false, @@ -425,6 +426,7 @@ providerName is null } private static TypeReference CreateConnectionType( + IDescriptorContext context, string? connectionName, TypeReference nodeType, bool includeTotalCount, @@ -437,13 +439,14 @@ private static TypeReference CreateConnectionType( _ => new ConnectionType(nodeType, includeTotalCount, includeNodesField), TypeContext.Output) : TypeReference.Create( - connectionName + "Connection", + NameHelper.CreateConnectionName(context.Naming, connectionName), TypeContext.Output, - factory: _ => new ConnectionType( + factory: c => new ConnectionType( connectionName, nodeType, includeTotalCount, - includeNodesField)); + includeNodesField, + c.Naming)); } private static string EnsureConnectionNameCasing(string connectionName) diff --git a/src/HotChocolate/Core/src/Types.CursorPagination/NameHelper.cs b/src/HotChocolate/Core/src/Types.CursorPagination/NameHelper.cs index 0b8e5cf3d01..9a5f2c88f8f 100644 --- a/src/HotChocolate/Core/src/Types.CursorPagination/NameHelper.cs +++ b/src/HotChocolate/Core/src/Types.CursorPagination/NameHelper.cs @@ -1,10 +1,16 @@ +using HotChocolate.Types.Descriptors; + namespace HotChocolate.Types.Pagination; internal static class NameHelper { - public static string CreateConnectionName(string connectionName) - => connectionName + "Connection"; + public static string CreateConnectionName( + INamingConventions namingConventions, + string connectionName) + => namingConventions.GetTypeName(connectionName + "Connection", TypeKind.Object); - public static string CreateEdgeName(string connectionName) - => connectionName + "Edge"; + public static string CreateEdgeName( + INamingConventions namingConventions, + string connectionName) + => namingConventions.GetTypeName(connectionName + "Edge", TypeKind.Object); } diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs index 6c4f9cadba2..c54b8955bf3 100644 --- a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs @@ -4,6 +4,7 @@ using HotChocolate.Internal; using HotChocolate.Resolvers; using HotChocolate.Tests; +using HotChocolate.Types.Descriptors; namespace HotChocolate.Types.Pagination; @@ -877,6 +878,36 @@ public async Task Explicit_ConnectionName() schema.MatchSnapshot(); } + [Fact] + public async Task Explicit_ConnectionName_With_NamingConvention() + { + var schema = + await new ServiceCollection() + .AddGraphQL() + .AddConvention() + .AddQueryType() + .Services + .BuildServiceProvider() + .GetSchemaAsync(); + + schema.MatchSnapshot(); + } + + [Fact] + public async Task Infer_ConnectionName_From_NodeType_With_NamingConvention() + { + var schema = + await new ServiceCollection() + .AddGraphQL() + .AddConvention() + .AddQueryType() + .Services + .BuildServiceProvider() + .GetSchemaAsync(); + + schema.MatchSnapshot(); + } + [Fact] public async Task SelectProviderByName() { @@ -1249,6 +1280,12 @@ public class InferConnectionNameFromField public string[] Names() => ["a", "b"]; } + public class InferConnectionNameFromNodeType + { + [UsePaging(InferConnectionNameFromField = false)] + public string[] Names() => ["a", "b"]; + } + public class ExplicitConnectionName { [UsePaging(ConnectionName = "Connection1")] @@ -1339,4 +1376,19 @@ public ImmutableArray Test() return []; } } + + private sealed class ConnectionNamingConventions : DefaultNamingConventions + { + public override string GetTypeName(string originalTypeName, TypeKind kind) + { + var name = base.GetTypeName(originalTypeName, kind); + + return kind is TypeKind.Object + && + (name.EndsWith("Connection", StringComparison.Ordinal) + || name.EndsWith("Edge", StringComparison.Ordinal)) + ? name + "Named" + : name; + } + } } diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Explicit_ConnectionName_With_NamingConvention.graphql b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Explicit_ConnectionName_With_NamingConvention.graphql new file mode 100644 index 00000000000..d6bfbfdf51f --- /dev/null +++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Explicit_ConnectionName_With_NamingConvention.graphql @@ -0,0 +1,75 @@ +schema { + query: ExplicitConnectionName +} + +"A connection to a list of items." +type Connection1ConnectionNamed { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [Connection1EdgeNamed!] + "A flattened list of the nodes." + nodes: [String!] +} + +"An edge in a connection." +type Connection1EdgeNamed { + "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 Connection2ConnectionNamed { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [Connection2EdgeNamed!] + "A flattened list of the nodes." + nodes: [String!] +} + +"An edge in a connection." +type Connection2EdgeNamed { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: String! +} + +type ExplicitConnectionName { + abc("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): Connection1ConnectionNamed + def("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): Connection2ConnectionNamed + ghi("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): GhiConnectionNamed +} + +"A connection to a list of items." +type GhiConnectionNamed { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [GhiEdgeNamed!] + "A flattened list of the nodes." + nodes: [String!] +} + +"An edge in a connection." +type GhiEdgeNamed { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: String! +} + +"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 +} diff --git a/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Infer_ConnectionName_From_NodeType_With_NamingConvention.graphql b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Infer_ConnectionName_From_NodeType_With_NamingConvention.graphql new file mode 100644 index 00000000000..4100cfea08b --- /dev/null +++ b/src/HotChocolate/Core/test/Types.CursorPagination.Tests/__snapshots__/IntegrationTests.Infer_ConnectionName_From_NodeType_With_NamingConvention.graphql @@ -0,0 +1,37 @@ +schema { + query: InferConnectionNameFromNodeType +} + +type InferConnectionNameFromNodeType { + names("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): StringConnectionNamed +} + +"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 +} + +"A connection to a list of items." +type StringConnectionNamed { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [StringEdgeNamed!] + "A flattened list of the nodes." + nodes: [String!] +} + +"An edge in a connection." +type StringEdgeNamed { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: String! +}