diff --git a/src/HotChocolate/Core/src/Execution/Processing/VariableCoercionHelper.cs b/src/HotChocolate/Core/src/Execution/Processing/VariableCoercionHelper.cs index f6dc2c4fbce..f74d91fbf50 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/VariableCoercionHelper.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/VariableCoercionHelper.cs @@ -52,13 +52,13 @@ public void CoerceVariableValues( { throw ThrowHelper.NonNullVariableIsNull(variableDefinition); } - coercedValues[variableName] = new VariableValueOrLiteral( - variableType, null, NullValueNode.Default); + coercedValues[variableName] = + new VariableValueOrLiteral(variableType, null, NullValueNode.Default); } else { - coercedValues[variableName] = CoerceVariableValue( - variableDefinition, variableType, value); + coercedValues[variableName] = + CoerceVariableValue(variableDefinition, variableType, value); } } } diff --git a/src/HotChocolate/Core/src/Execution/Processing/VariableRewriter.cs b/src/HotChocolate/Core/src/Execution/Processing/VariableRewriter.cs index 227d6bd318d..f550ac745ec 100644 --- a/src/HotChocolate/Core/src/Execution/Processing/VariableRewriter.cs +++ b/src/HotChocolate/Core/src/Execution/Processing/VariableRewriter.cs @@ -78,6 +78,10 @@ private static ObjectValueNode Rewrite( } rewrittenItems[i] = rewritten; } + else if (rewrittenItems is { }) + { + rewrittenItems[i] = node.Fields[i]; + } } if (rewrittenItems is { }) @@ -138,6 +142,10 @@ private static ListValueNode Rewrite( } rewrittenItems[i] = rewritten; } + else if (rewrittenItems is { }) + { + rewrittenItems[i] = node.Items[i]; + } } if (rewrittenItems is { }) diff --git a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs index 8ef1187de1d..5e48507a4d3 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableFilterProvider.cs @@ -55,12 +55,11 @@ context.LocalContextData[ContextValueNodeKey] is IValueNode node : context.ArgumentLiteral(argumentName); // if no filter is defined we can stop here and yield back control. - if (filter.IsNull() || - (context.LocalContextData.TryGetValue( - SkipFilteringKey, - out object? skipObject) && - skipObject is bool skip && - skip)) + var skipFiltering = + context.LocalContextData.TryGetValue(SkipFilteringKey, out object? skip) && + skip is true; + + if (filter.IsNull() || skipFiltering) { return; } @@ -72,15 +71,12 @@ skipObject is bool skip && executorObj is VisitFilterArgument executor) { var inMemory = - context.Result is QueryableExecutable executable && - executable.InMemory || + context.Result is QueryableExecutable { InMemory: true } || context.Result is not IQueryable || context.Result is EnumerableQuery; - QueryableFilterContext visitorContext = executor( - filter, - filterInput, - inMemory); + QueryableFilterContext visitorContext = + executor(filter, filterInput, inMemory); // compile expression tree if (visitorContext.TryCreateLambda( diff --git a/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProvider.cs b/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProvider.cs index 5020ec3528c..5e18e5cdf6c 100644 --- a/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProvider.cs +++ b/src/HotChocolate/Data/src/Data/Sorting/Expressions/QueryableSortProvider.cs @@ -44,12 +44,11 @@ async ValueTask ExecuteAsync( IValueNode sort = context.ArgumentLiteral(argumentName); // if no sort is defined we can stop here and yield back control. - if (sort.IsNull() || - (context.LocalContextData.TryGetValue( - SkipSortingKey, - out object? skipObject) && - skipObject is bool skip && - skip)) + var skipSorting = + context.LocalContextData.TryGetValue(SkipSortingKey, out object? skip) && + skip is true; + + if (sort.IsNull() || skipSorting) { return; } @@ -63,8 +62,7 @@ lt.ElementType is NonNullType nn && executorObj is VisitSortArgument executor) { var inMemory = - context.Result is QueryableExecutable executable && - executable.InMemory || + context.Result is QueryableExecutable { InMemory: true } || context.Result is not IQueryable || context.Result is EnumerableQuery; diff --git a/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs b/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs index 6ccb6c24c80..b8a85dbcb6d 100644 --- a/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs +++ b/src/HotChocolate/Data/test/Data.Tests/IntegrationTests.cs @@ -577,6 +577,70 @@ public async Task CreateSchema_OnDifferentScope() result.ToJson().MatchSnapshot(new SnapshotNameExtension("Result")); } + [Fact] + public async Task Execute_And_OnRoot() + { + // arrange + IRequestExecutor executor = await new ServiceCollection() + .AddGraphQL() + .AddFiltering("Foo") + .AddSorting("Foo") + .AddProjections("Foo") + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + IExecutionResult result = await executor.ExecuteAsync( + @" + query GetBooks($title: String) { + books(where: { + and: [ + { title: { startsWith: $title } }, + { title: { eq: ""BookTitle"" } }, + ] + }) { + nodes { title } + } + }", + new Dictionary { ["title"] = "BookTitle" }); + + // assert + executor.Schema.Print().MatchSnapshot(new SnapshotNameExtension("Schema")); + result.ToJson().MatchSnapshot(new SnapshotNameExtension("Result")); + } + + [Fact] + public async Task Execute_And_OnRoot_Reverse() + { + // arrange + IRequestExecutor executor = await new ServiceCollection() + .AddGraphQL() + .AddFiltering("Foo") + .AddSorting("Foo") + .AddProjections("Foo") + .AddQueryType() + .BuildRequestExecutorAsync(); + + // act + IExecutionResult result = await executor.ExecuteAsync( + @" + query GetBooks($title: String) { + books(where: { + and: [ + { title: { eq: ""BookTitle"" } }, + { title: { startsWith: $title } }, + ] + }) { + nodes { title } + } + }", + new Dictionary { ["title"] = "BookTitle" }); + + // assert + executor.Schema.Print().MatchSnapshot(new SnapshotNameExtension("Schema")); + result.ToJson().MatchSnapshot(new SnapshotNameExtension("Result")); + } + public class FooType : ObjectType { protected override void Configure(IObjectTypeDescriptor descriptor) diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Result.snap b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Result.snap new file mode 100644 index 00000000000..8c3fc5babb3 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Result.snap @@ -0,0 +1,11 @@ +{ + "data": { + "books": { + "nodes": [ + { + "title": "BookTitle" + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Reverse_Result.snap b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Reverse_Result.snap new file mode 100644 index 00000000000..8c3fc5babb3 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Reverse_Result.snap @@ -0,0 +1,11 @@ +{ + "data": { + "books": { + "nodes": [ + { + "title": "BookTitle" + } + ] + } + } +} diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Reverse_Schema.snap b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Reverse_Schema.snap new file mode 100644 index 00000000000..39abb60d465 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Reverse_Schema.snap @@ -0,0 +1,127 @@ +schema { + query: DifferentScope +} + +type Author { + id: Int! + name: String + books: [Book!]! +} + +type Book { + id: Int! + authorId: Int! + title: String + author: Author +} + +"A connection to a list of items." +type BookConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [BookEdge!] + "A flattened list of the nodes." + nodes: [Book!] +} + +"An edge in a connection." +type BookEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Book! +} + +type DifferentScope { + books(first: Int after: String last: Int before: String where: Foo_BookFilterInput order: [Foo_BookSortInput!]): BookConnection +} + +"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 +} + +input Foo_AuthorFilterInput { + and: [Foo_AuthorFilterInput!] + or: [Foo_AuthorFilterInput!] + id: Foo_ComparableInt32OperationFilterInput + name: Foo_StringOperationFilterInput + books: Foo_ListFilterInputTypeOfBookFilterInput +} + +input Foo_AuthorSortInput { + id: Foo_SortEnumType + name: Foo_SortEnumType +} + +input Foo_BookFilterInput { + and: [Foo_BookFilterInput!] + or: [Foo_BookFilterInput!] + id: Foo_ComparableInt32OperationFilterInput + authorId: Foo_ComparableInt32OperationFilterInput + title: Foo_StringOperationFilterInput + author: Foo_AuthorFilterInput +} + +input Foo_BookSortInput { + id: Foo_SortEnumType + authorId: Foo_SortEnumType + title: Foo_SortEnumType + author: Foo_AuthorSortInput +} + +input Foo_ComparableInt32OperationFilterInput { + eq: Int + neq: Int + in: [Int!] + nin: [Int!] + gt: Int + ngt: Int + gte: Int + ngte: Int + lt: Int + nlt: Int + lte: Int + nlte: Int +} + +input Foo_ListFilterInputTypeOfBookFilterInput { + all: Foo_BookFilterInput + none: Foo_BookFilterInput + some: Foo_BookFilterInput + any: Boolean +} + +input Foo_StringOperationFilterInput { + and: [Foo_StringOperationFilterInput!] + or: [Foo_StringOperationFilterInput!] + eq: String + neq: String + contains: String + ncontains: String + in: [String] + nin: [String] + startsWith: String + nstartsWith: String + endsWith: String + nendsWith: String +} + +enum Foo_SortEnumType { + ASC + DESC +} + +"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." +directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." +directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! "Streamed when true." if: Boolean!) on FIELD diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Schema.snap b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Schema.snap new file mode 100644 index 00000000000..39abb60d465 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/IntegrationTests.Execute_And_OnRoot_Schema.snap @@ -0,0 +1,127 @@ +schema { + query: DifferentScope +} + +type Author { + id: Int! + name: String + books: [Book!]! +} + +type Book { + id: Int! + authorId: Int! + title: String + author: Author +} + +"A connection to a list of items." +type BookConnection { + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [BookEdge!] + "A flattened list of the nodes." + nodes: [Book!] +} + +"An edge in a connection." +type BookEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Book! +} + +type DifferentScope { + books(first: Int after: String last: Int before: String where: Foo_BookFilterInput order: [Foo_BookSortInput!]): BookConnection +} + +"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 +} + +input Foo_AuthorFilterInput { + and: [Foo_AuthorFilterInput!] + or: [Foo_AuthorFilterInput!] + id: Foo_ComparableInt32OperationFilterInput + name: Foo_StringOperationFilterInput + books: Foo_ListFilterInputTypeOfBookFilterInput +} + +input Foo_AuthorSortInput { + id: Foo_SortEnumType + name: Foo_SortEnumType +} + +input Foo_BookFilterInput { + and: [Foo_BookFilterInput!] + or: [Foo_BookFilterInput!] + id: Foo_ComparableInt32OperationFilterInput + authorId: Foo_ComparableInt32OperationFilterInput + title: Foo_StringOperationFilterInput + author: Foo_AuthorFilterInput +} + +input Foo_BookSortInput { + id: Foo_SortEnumType + authorId: Foo_SortEnumType + title: Foo_SortEnumType + author: Foo_AuthorSortInput +} + +input Foo_ComparableInt32OperationFilterInput { + eq: Int + neq: Int + in: [Int!] + nin: [Int!] + gt: Int + ngt: Int + gte: Int + ngte: Int + lt: Int + nlt: Int + lte: Int + nlte: Int +} + +input Foo_ListFilterInputTypeOfBookFilterInput { + all: Foo_BookFilterInput + none: Foo_BookFilterInput + some: Foo_BookFilterInput + any: Boolean +} + +input Foo_StringOperationFilterInput { + and: [Foo_StringOperationFilterInput!] + or: [Foo_StringOperationFilterInput!] + eq: String + neq: String + contains: String + ncontains: String + in: [String] + nin: [String] + startsWith: String + nstartsWith: String + endsWith: String + nendsWith: String +} + +enum Foo_SortEnumType { + ASC + DESC +} + +"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." +directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." +directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! "Streamed when true." if: Boolean!) on FIELD