Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed issue with null value serialization when using GraphQL literals. #6357

Merged
merged 5 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal static void WriteFieldValue(
Utf8JsonWriter writer,
object? value)
{
if (value is null or FileReference or FileReferenceNode)
if (value is null or NullValueNode or FileReference or FileReferenceNode)
{
writer.WriteNullValue();
return;
Expand Down
49 changes: 48 additions & 1 deletion src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1343,7 +1343,7 @@ query Requires {

Assert.Null(result.ExpectQueryResult().Errors);
}

[Fact]
public async Task Require_Data_In_Context_3()
{
Expand Down Expand Up @@ -1410,6 +1410,53 @@ query Large {
Assert.Null(result.ExpectQueryResult().Errors);
}

[Fact]
public async Task GetFirstPage_With_After_Null()
{
using var demoProject = await DemoProject.CreateAsync();

// act
var fusionGraph = await new FusionGraphComposer(logFactory: _logFactory).ComposeAsync(
new[]
{
demoProject.Appointment.ToConfiguration()
},
new FusionFeatureCollection(FusionFeatures.NodeField));

var executor = await new ServiceCollection()
.AddSingleton(demoProject.HttpClientFactory)
.AddSingleton(demoProject.WebSocketConnectionFactory)
.AddFusionGatewayServer()
.ConfigureFromDocument(SchemaFormatter.FormatAsDocument(fusionGraph))
.BuildRequestExecutorAsync();

var request = Parse(
"""
query AfterNull($after: String) {
appointments(after: $after) {
nodes {
id
}
}
}
""");

// act
var result = await executor.ExecuteAsync(
QueryRequestBuilder
.New()
.SetQuery(request)
.SetVariableValue("after", null)
.Create());

// assert
var snapshot = new Snapshot();
CollectSnapshotData(snapshot, request, result, fusionGraph);
await snapshot.MatchAsync();

Assert.Null(result.ExpectQueryResult().Errors);
}

public sealed class HotReloadConfiguration : IObservable<GatewayConfiguration>
{
private GatewayConfiguration _configuration;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
User Request
---------------
query AfterNull($after: String) {
appointments(after: $after) {
nodes {
id
}
}
}
---------------

QueryPlan
---------------
{
"document": "query AfterNull($after: String) { appointments(after: $after) { nodes { id } } }",
"operation": "AfterNull",
"rootNode": {
"type": "Sequence",
"nodes": [
{
"type": "Resolve",
"subgraph": "Appointment",
"document": "query AfterNull_1($after: String) { appointments(after: $after, before: null, first: null, last: null) { nodes { id } } }",
"selectionSetId": 0,
"forwardedVariables": [
{
"variable": "after"
}
]
},
{
"type": "Compose",
"selectionSetIds": [
0
]
}
]
}
}
---------------

Result
---------------
{
"data": {
"appointments": {
"nodes": [
{
"id": "QXBwb2ludG1lbnQKaTE="
},
{
"id": "QXBwb2ludG1lbnQKaTI="
}
]
}
}
}
---------------

Fusion Graph
---------------
schema @fusion(version: 1) @httpClient(subgraph: "Appointment", baseAddress: "http:\/\/localhost:5000\/graphql") @webSocketClient(subgraph: "Appointment", baseAddress: "ws:\/\/localhost:5000\/graphql") @node(subgraph: "Appointment", types: [ "Patient1", "Appointment" ]) {
query: Query
}

type Query {
appointmentById(appointmentId: ID!): Appointment @variable(subgraph: "Appointment", name: "appointmentId", argument: "appointmentId") @resolver(subgraph: "Appointment", select: "{ appointmentById(appointmentId: $appointmentId) }", arguments: [ { name: "appointmentId", type: "ID!" } ])
appointments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): AppointmentsConnection @variable(subgraph: "Appointment", name: "after", argument: "after") @variable(subgraph: "Appointment", name: "before", argument: "before") @variable(subgraph: "Appointment", name: "first", argument: "first") @variable(subgraph: "Appointment", name: "last", argument: "last") @resolver(subgraph: "Appointment", select: "{ appointments(after: $after, before: $before, first: $first, last: $last) }", arguments: [ { name: "after", type: "String" }, { name: "before", type: "String" }, { name: "first", type: "Int" }, { name: "last", type: "Int" } ])
"Fetches an object given its ID."
node("ID of the object." id: ID!): Node @variable(subgraph: "Appointment", name: "id", argument: "id") @resolver(subgraph: "Appointment", select: "{ node(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
"Lookup nodes by a list of IDs."
nodes("The list of node IDs." ids: [ID!]!): [Node]! @variable(subgraph: "Appointment", name: "ids", argument: "ids") @resolver(subgraph: "Appointment", select: "{ nodes(ids: $ids) }", arguments: [ { name: "ids", type: "[ID!]!" } ])
patient(id: ID!): Patient1 @variable(subgraph: "Appointment", name: "id", argument: "id") @resolver(subgraph: "Appointment", select: "{ patient(id: $id) }", arguments: [ { name: "id", type: "ID!" } ])
}

type Appointment implements Node @variable(subgraph: "Appointment", name: "Appointment_id", select: "id") @resolver(subgraph: "Appointment", select: "{ node(id: $Appointment_id) { ... on Appointment { ... Appointment } } }", arguments: [ { name: "Appointment_id", type: "ID!" } ]) @resolver(subgraph: "Appointment", select: "{ nodes(ids: $Appointment_id) { ... on Appointment { ... Appointment } } }", arguments: [ { name: "Appointment_id", type: "[ID!]!" } ], kind: "BATCH_BY_KEY") {
id: ID! @source(subgraph: "Appointment")
patient: IPatient! @source(subgraph: "Appointment")
}

"A connection to a list of items."
type AppointmentsConnection {
"A list of edges."
edges: [AppointmentsEdge!] @source(subgraph: "Appointment")
"A flattened list of the nodes."
nodes: [Appointment!] @source(subgraph: "Appointment")
"Information to aid in pagination."
pageInfo: PageInfo! @source(subgraph: "Appointment")
}

"An edge in a connection."
type AppointmentsEdge {
"A cursor for use in pagination."
cursor: String! @source(subgraph: "Appointment")
"The item at the end of the edge."
node: Appointment! @source(subgraph: "Appointment")
}

"Information about pagination in a connection."
type PageInfo {
"When paginating forwards, the cursor to continue."
endCursor: String @source(subgraph: "Appointment")
"Indicates whether more edges exist following the set defined by the clients arguments."
hasNextPage: Boolean! @source(subgraph: "Appointment")
"Indicates whether more edges exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean! @source(subgraph: "Appointment")
"When paginating backwards, the cursor to continue."
startCursor: String @source(subgraph: "Appointment")
}

type Patient1 implements IPatient & Node @variable(subgraph: "Appointment", name: "Patient1_id", select: "id") @resolver(subgraph: "Appointment", select: "{ node(id: $Patient1_id) { ... on Patient1 { ... Patient1 } } }", arguments: [ { name: "Patient1_id", type: "ID!" } ]) @resolver(subgraph: "Appointment", select: "{ nodes(ids: $Patient1_id) { ... on Patient1 { ... Patient1 } } }", arguments: [ { name: "Patient1_id", type: "[ID!]!" } ], kind: "BATCH_BY_KEY") {
appointments("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int): AppointmentsConnection @source(subgraph: "Appointment") @variable(subgraph: "Appointment", name: "after", argument: "after") @variable(subgraph: "Appointment", name: "before", argument: "before") @variable(subgraph: "Appointment", name: "first", argument: "first") @variable(subgraph: "Appointment", name: "last", argument: "last")
id: ID! @source(subgraph: "Appointment")
}

type Patient2 implements IPatient {
id: ID! @source(subgraph: "Appointment")
}

interface IPatient {
id: ID!
}

interface Node {
id: ID!
}
---------------