diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed1a95cdc1..159b99d5f18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Subscription now uses pipeline API to abstract sockets. [#807](https://github.com/ChilliCream/hotchocolate/pull/807) - Improved parser performance. [#806](https://github.com/ChilliCream/hotchocolate/pull/806) +## [9.0.4] - 2019-06-16 + +### Fixed + +- Fixed paging flaws that in some cases lead to the connection type being registered twice. [#842](https://github.com/ChilliCream/hotchocolate/pull/842) + +## [9.0.3] - 2019-06-13 + +### Fixed + +- Fixed issues where the type initializer would swallow schema errors. + ## [9.0.2] - 2019-06-12 ### Fixed diff --git a/examples/AspNetCore.StarWars/.vscode/launch.json b/examples/AspNetCore.StarWars/.vscode/launch.json new file mode 100644 index 00000000000..401f8073c3b --- /dev/null +++ b/examples/AspNetCore.StarWars/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/AspNetCore.StarWars.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "serverReadyAction": { + "action": "openExternally", + "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/examples/AspNetCore.StarWars/.vscode/tasks.json b/examples/AspNetCore.StarWars/.vscode/tasks.json new file mode 100644 index 00000000000..68ae5ffa3e2 --- /dev/null +++ b/examples/AspNetCore.StarWars/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet build", + "type": "shell", + "group": "build", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": "$msCompile" + } + ] +} diff --git a/examples/AspNetCore.StarWars/Types/CharacterType.cs b/examples/AspNetCore.StarWars/Types/CharacterType.cs index ef528b04cde..b9c5c98c5f9 100644 --- a/examples/AspNetCore.StarWars/Types/CharacterType.cs +++ b/examples/AspNetCore.StarWars/Types/CharacterType.cs @@ -1,4 +1,5 @@ using HotChocolate.Types; +using HotChocolate.Types.Relay; using StarWars.Models; namespace StarWars.Types @@ -17,7 +18,7 @@ protected override void Configure(IInterfaceTypeDescriptor descripto .Type(); descriptor.Field(f => f.Friends) - .Type>(); + .UsePaging(); descriptor.Field(f => f.AppearsIn) .Type>(); diff --git a/examples/AspNetCore.StarWars/Types/DroidType.cs b/examples/AspNetCore.StarWars/Types/DroidType.cs index 0a9764feae2..c34fe9a3864 100644 --- a/examples/AspNetCore.StarWars/Types/DroidType.cs +++ b/examples/AspNetCore.StarWars/Types/DroidType.cs @@ -1,4 +1,5 @@ using HotChocolate.Types; +using HotChocolate.Types.Relay; using StarWars.Models; using StarWars.Resolvers; @@ -18,7 +19,7 @@ protected override void Configure(IObjectTypeDescriptor descriptor) .Type>(); descriptor.Field(r => r.GetCharacter(default, default)) - .Type>() + .UsePaging() .Name("friends"); descriptor.Field(t => t.GetHeight(default, default)) diff --git a/examples/AspNetCore.StarWars/Types/HumanType.cs b/examples/AspNetCore.StarWars/Types/HumanType.cs index cb04f0e7c8f..3ab1c6a0d8f 100644 --- a/examples/AspNetCore.StarWars/Types/HumanType.cs +++ b/examples/AspNetCore.StarWars/Types/HumanType.cs @@ -1,4 +1,5 @@ using HotChocolate.Types; +using HotChocolate.Types.Relay; using StarWars.Models; using StarWars.Resolvers; @@ -18,7 +19,7 @@ protected override void Configure(IObjectTypeDescriptor descriptor) .Type>(); descriptor.Field(r => r.GetCharacter(default, default)) - .Type>() + .UsePaging() .Name("friends"); descriptor.Field(t => t.GetHeight(default, default)) diff --git a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs index b94a400dada..6f481e65cc7 100644 --- a/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs +++ b/src/Core/Types.Tests/Types/Relay/ConnectionTypeTests.cs @@ -43,7 +43,7 @@ public void CheckFieldsAreCorrect() }, t => { - Assert.Equal("items", t.Name); + Assert.Equal("nodes", t.Name); Assert.IsType( Assert.IsType(t.Type).ElementType); }, @@ -104,7 +104,7 @@ public async Task UsePaging_WithNonNull_ElementType() cursor node } - items + nodes pageInfo { hasNextPage @@ -122,6 +122,43 @@ public async Task UsePaging_WithNonNull_ElementType() result.MatchSnapshot(); } + [Fact] + public async Task UsePaging_WithComplexType() + { + // arrange + ISchema schema = SchemaBuilder.New() + .AddType() + .AddQueryType() + .Create(); + + IQueryExecutor executor = schema.MakeExecutable(); + + string query = @" + { + s + { + bar { + edges { + cursor + node + } + pageInfo + { + hasNextPage + } + totalCount + } + } + } + "; + + // act + IExecutionResult result = await executor.ExecuteAsync(query); + + // assert + result.MatchSnapshot(); + } + public class QueryType : ObjectType { @@ -152,10 +189,50 @@ protected override void Configure(IObjectTypeDescriptor descriptor) } } + public class QueryType3 + : ObjectType + { + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name("Query"); + descriptor.Field("s") + .Resolver(ctx => new Foo()); + } + } + public class Query { public ICollection Strings { get; } = new List { "a", "b", "c", "d", "e", "f", "g" }; } + + public class FooType + : ObjectType + { + protected override void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Interface(); + descriptor.Field(t => t.Bar).UsePaging(); + } + } + + public class FooInterfaceType + : InterfaceType + { + protected override void Configure( + IInterfaceTypeDescriptor descriptor) + { + descriptor.Name("IFoo"); + descriptor.Field("bar") + .UsePaging(); + } + } + + public class Foo + { + public ICollection Bar { get; } = + new List { "a", "b", "c", "d", "e", "f", "g" }; + } } } diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithComplexType.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithComplexType.snap new file mode 100644 index 00000000000..a090851e24b --- /dev/null +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithComplexType.snap @@ -0,0 +1,45 @@ +{ + "Data": { + "s": { + "bar": { + "edges": [ + { + "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjB9", + "node": "a" + }, + { + "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjF9", + "node": "b" + }, + { + "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjJ9", + "node": "c" + }, + { + "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjN9", + "node": "d" + }, + { + "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjR9", + "node": "e" + }, + { + "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjV9", + "node": "f" + }, + { + "cursor": "eyJfX3RvdGFsQ291bnQiOjcsIl9fcG9zaXRpb24iOjZ9", + "node": "g" + } + ], + "pageInfo": { + "hasNextPage": false + }, + "totalCount": 7 + } + } + }, + "Extensions": {}, + "Errors": [], + "ContextData": {} +} diff --git a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithNonNull_ElementType.snap b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithNonNull_ElementType.snap index b223a4aec24..a018131d5a1 100644 --- a/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithNonNull_ElementType.snap +++ b/src/Core/Types.Tests/Types/Relay/__snapshots__/ConnectionTypeTests.UsePaging_WithNonNull_ElementType.snap @@ -11,7 +11,7 @@ "node": "g" } ], - "items": [ + "nodes": [ "f", "g" ], diff --git a/src/Core/Types/Types/Relay/ConnectionType.cs b/src/Core/Types/Types/Relay/ConnectionType.cs index 6fd4a357787..1f2e12b5248 100644 --- a/src/Core/Types/Types/Relay/ConnectionType.cs +++ b/src/Core/Types/Types/Relay/ConnectionType.cs @@ -1,7 +1,6 @@ using System; using System.Linq; using HotChocolate.Configuration; -using HotChocolate.Resolvers; using HotChocolate.Types.Descriptors; using HotChocolate.Types.Descriptors.Definitions; @@ -29,7 +28,6 @@ public ConnectionType( public IEdgeType EdgeType { get; private set; } - protected new static void Configure( IObjectTypeDescriptor descriptor) { @@ -50,7 +48,7 @@ public ConnectionType( .Description("A list of edges.") .Type>>>(); - descriptor.Field("items") + descriptor.Field("nodes") .Description("A flattened list of the nodes.") .Type>() .Resolver(ctx => @@ -77,28 +75,5 @@ protected override void OnCompleteType( EdgeType = context.GetType>( ClrTypeReference.FromSchemaType>()); } - - public static ConnectionType CreateWithTotalCount() - { - return new ConnectionType(c => - { - c.Field("totalCount") - .Type>() - .Resolver(ctx => GetTotalCount(ctx)); - }); - } - - private static IResolverResult GetTotalCount( - IResolverContext context) - { - IConnection connection = context.Parent(); - if (connection.PageInfo.TotalCount.HasValue) - { - return ResolverResult.CreateValue( - connection.PageInfo.TotalCount.Value); - } - return ResolverResult.CreateError( - "The total count was not provided by the connection."); - } } } diff --git a/src/Core/Types/Types/Relay/ConnectionWithCountType.cs b/src/Core/Types/Types/Relay/ConnectionWithCountType.cs new file mode 100644 index 00000000000..758a783cb5d --- /dev/null +++ b/src/Core/Types/Types/Relay/ConnectionWithCountType.cs @@ -0,0 +1,46 @@ +using System; +using HotChocolate.Resolvers; + +namespace HotChocolate.Types.Relay +{ + public class ConnectionWithCountType + : ConnectionType + where T : IOutputType + { + public ConnectionWithCountType() + : base(descriptor => Configure(descriptor)) + { + } + + public ConnectionWithCountType( + Action> configure) + : base(descriptor => + { + Configure(descriptor); + configure?.Invoke(descriptor); + }) + { + } + + protected new static void Configure( + IObjectTypeDescriptor descriptor) + { + descriptor.Field("totalCount") + .Type>() + .Resolver(ctx => GetTotalCount(ctx)); + } + + private static IResolverResult GetTotalCount( + IResolverContext context) + { + IConnection connection = context.Parent(); + if (connection.PageInfo.TotalCount.HasValue) + { + return ResolverResult.CreateValue( + connection.PageInfo.TotalCount.Value); + } + return ResolverResult.CreateError( + "The total count was not provided by the connection."); + } + } +} diff --git a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs index 0f2a30e3204..67263ddb000 100644 --- a/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs +++ b/src/Core/Types/Types/Relay/Extensions/PagingObjectFieldDescriptorExtensions.cs @@ -13,7 +13,7 @@ public static IObjectFieldDescriptor UsePaging( { return descriptor .AddPagingArguments() - .Type(ConnectionType.CreateWithTotalCount()) + .Type>() .Use>(); } @@ -27,7 +27,7 @@ public static IObjectFieldDescriptor UsePaging( descriptor .AddPagingArguments() - .Type(ConnectionType.CreateWithTotalCount()) + .Type>() .Use(placeholder) .Extend() .OnBeforeCompletion((context, defintion) => @@ -52,6 +52,17 @@ public static IObjectFieldDescriptor UsePaging( return descriptor; } + public static IInterfaceFieldDescriptor UsePaging( + this IInterfaceFieldDescriptor descriptor) + where TSchemaType : class, IOutputType + { + descriptor + .AddPagingArguments() + .Type>(); + + return descriptor; + } + public static IObjectFieldDescriptor AddPagingArguments( this IObjectFieldDescriptor descriptor) { @@ -61,5 +72,15 @@ public static IObjectFieldDescriptor AddPagingArguments( .Argument("last", a => a.Type()) .Argument("before", a => a.Type()); } + + public static IInterfaceFieldDescriptor AddPagingArguments( + this IInterfaceFieldDescriptor descriptor) + { + return descriptor + .Argument("first", a => a.Type()) + .Argument("after", a => a.Type()) + .Argument("last", a => a.Type()) + .Argument("before", a => a.Type()); + } } } diff --git a/src/Stitching/Stitching.Tests/__snapshots__/StitchingBuilderTests.AddSchemaFromHttp.snap b/src/Stitching/Stitching.Tests/__snapshots__/StitchingBuilderTests.AddSchemaFromHttp.snap index d51e4827e5e..ddcc5243376 100644 --- a/src/Stitching/Stitching.Tests/__snapshots__/StitchingBuilderTests.AddSchemaFromHttp.snap +++ b/src/Stitching/Stitching.Tests/__snapshots__/StitchingBuilderTests.AddSchemaFromHttp.snap @@ -43,7 +43,7 @@ type CustomerConnection @source(name: "CustomerConnection", schema: "customer") "A list of edges." edges: [CustomerEdge!] "A flattened list of the nodes." - items: [Customer] + nodes: [Customer] "Information to aid in pagination." pageInfo: PageInfo! totalCount: Int! diff --git a/src/Stitching/Stitching.Tests/__snapshots__/StitchingBuilderTests.AddSchema_2.snap b/src/Stitching/Stitching.Tests/__snapshots__/StitchingBuilderTests.AddSchema_2.snap index 3335da1377c..df04971587d 100644 --- a/src/Stitching/Stitching.Tests/__snapshots__/StitchingBuilderTests.AddSchema_2.snap +++ b/src/Stitching/Stitching.Tests/__snapshots__/StitchingBuilderTests.AddSchema_2.snap @@ -41,7 +41,7 @@ type CustomerConnection @source(name: "CustomerConnection", schema: "customer") "A list of edges." edges: [CustomerEdge!] "A flattened list of the nodes." - items: [Customer] + nodes: [Customer] "Information to aid in pagination." pageInfo: PageInfo! totalCount: Int!