Skip to content

Commit 1cde15c

Browse files
committed
- Added better support for Mutation handling so that single payload (per Mutation convention best practices) can be returned easily via ReceiveGraphQLMutationResult; this eliminates the need to use ReceiveGraphQLRawJsonResponse for dynamic Mutation response handling.
- Fixed bug to ensure Errors are returned on IGraphQLQueryResults when possible (not available on Batch Queries). - Fixed bug in processing logic for paginated reqquests when TotalCount is the only selected field on a paginated request; only affected CollectionSegment/Offset Paging requests.
1 parent 0e94d68 commit 1cde15c

File tree

8 files changed

+131
-21
lines changed

8 files changed

+131
-21
lines changed

FlurlGraphQL.Querying/Flurl/FlurlGraphQLRequest.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Collections.ObjectModel;
45
using System.Linq;
@@ -34,6 +35,8 @@ internal FlurlGraphQLRequest(IFlurlRequest baseRequest)
3435

3536
public GraphQLQueryType GraphQLQueryType { get; protected set; }
3637

38+
public bool IsMutationQuery { get; protected set; }
39+
3740

3841
#region GraphQL Variables
3942

@@ -121,6 +124,7 @@ public IFlurlGraphQLRequest WithGraphQLQuery(string query, NullValueHandling nul
121124
ClearGraphQLQuery();
122125
GraphQLQuery = query;
123126
GraphQLQueryType = GraphQLQueryType.Query;
127+
IsMutationQuery = DetermineIfMutationQuery(query);
124128
}
125129

126130
return this;
@@ -139,13 +143,21 @@ public IFlurlGraphQLRequest WithGraphQLPersistedQuery(string id, NullValueHandli
139143
ClearGraphQLQuery();
140144
GraphQLQuery = id;
141145
GraphQLQueryType = GraphQLQueryType.PersistedQuery;
146+
IsMutationQuery = false;
142147
}
143148

144149
return this;
145150
}
146151

147152
#endregion
148153

154+
#region QueryParsing Helpers
155+
156+
protected bool DetermineIfMutationQuery(string query)
157+
=> query?.TrimStart().StartsWith("mutation", StringComparison.OrdinalIgnoreCase) ?? false;
158+
159+
#endregion
160+
149161
#region ClearGraphQLQuery(), Clone()
150162

151163
public IFlurlGraphQLRequest ClearGraphQLQuery()
@@ -235,8 +247,7 @@ protected string BuildPostRequestJsonPayload()
235247
{
236248
//Execute the Query with the GraphQL Server...
237249
//var graphqlPayload = new FlurlGraphQLRequestPayloadBuilder(graphqlQueryType, graphqlQueryOrId, this.GraphQLVariablesInternal);
238-
var graphqlPayload = new Dictionary<string, object>();
239-
graphqlPayload.Add("variables", this.GraphQLVariablesInternal);
250+
var graphqlPayload = new Dictionary<string, object> { { "variables", this.GraphQLVariablesInternal } };
240251

241252
switch (GraphQLQueryType)
242253
{

FlurlGraphQL.Querying/Flurl/FlurlGraphQLResponseExtensions.Internal.cs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -273,26 +273,31 @@ internal static IGraphQLQueryResults<TEntityResult> ParseJsonToGraphQLResultsWit
273273
case JArray arrayResults:
274274
entityResults = arrayResults.ToObject<List<TEntityResult>>(jsonSerializer);
275275
break;
276+
// ReSharper disable once MergeIntoPattern
276277
case JObject jsonObj when jsonObj.First is JArray firstArrayResults:
277278
entityResults = firstArrayResults.ToObject<List<TEntityResult>>(jsonSerializer);
278279
break;
280+
//If only a single Object was returned then this is likely a Mutation so we return the single
281+
// item as the first-and-only result of the set...
282+
case JObject jsonObj:
283+
var singleResult = jsonObj.ToObject<TEntityResult>(jsonSerializer);
284+
entityResults = new List<TEntityResult>() { singleResult };
285+
break;
279286
}
280287
}
281288

282289
//If the results have Paging Info we map to the correct type (Connection/Cursor or CollectionSegment/Offset)...
283-
//NOTE: If we have a Total Count then we also must return a Paging result because it's possible to
284-
// request TotalCount by itself without any other PageInfo or Nodes...
285-
if (paginationType == PaginationType.Cursor || totalCount.HasValue)
286-
{
290+
if (paginationType == PaginationType.Cursor)
287291
return new GraphQLConnectionResults<TEntityResult>(entityResults, totalCount, pageInfo);
288-
}
289292
else if (paginationType == PaginationType.Offset)
290-
{
291293
return new GraphQLCollectionSegmentResults<TEntityResult>(entityResults, totalCount, GraphQLOffsetPageInfo.FromCursorPageInfo(pageInfo));
292-
}
293-
294+
//If we have a Total Count then we also must return a Paging result because it's possible to request TotalCount by itself without any other PageInfo or Nodes...
295+
//NOTE: WE must check this AFTER we process based on Cursor Type to make sure Cursor/Offset are both handled (if specified)...
296+
else if (totalCount.HasValue)
297+
return new GraphQLConnectionResults<TEntityResult>(entityResults, totalCount, pageInfo);
294298
//If not a paging result then we simply return the typed results...
295-
return new GraphQLQueryResults<TEntityResult>(entityResults);
299+
else
300+
return new GraphQLQueryResults<TEntityResult>(entityResults);
296301
}
297302

298303
internal static JArray FlattenGraphQLEdgesJsonToArrayOfNodes(this JArray edgesJson)

FlurlGraphQL.Querying/Flurl/FlurlGraphQLResponseExtensions.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using Newtonsoft.Json.Linq;
33
using System.Collections.Generic;
4+
using System.Linq;
45
using System.Threading.Tasks;
56
using System.Threading;
67

@@ -41,6 +42,42 @@ public static async Task<IGraphQLQueryResults<TResult>> ReceiveGraphQLQueryResul
4142
public static Task<IGraphQLQueryResults<TResult>> ReceiveGraphQLQueryResults<TResult>(this IFlurlGraphQLResponse response, string queryOperationName = null)
4243
where TResult : class => Task.FromResult(response).ReceiveGraphQLQueryResults<TResult>(queryOperationName);
4344

45+
/// <summary>
46+
/// Processes/parses the results of the GraphQL mutation execution into the specified result payload.
47+
/// This assumes that the Mutation conventions used follow best practices in that GraphQL mutations should take in and Input payload
48+
/// and return a result Payload; the result payload may be the single object type or a root type for a collection of results & errors (as defined by the GraphQL Schema).
49+
/// However, if you need more control over the processing of the Results you can dynamically handle it with the ReceiveGraphQLRawJsonResponse() method.
50+
/// </summary>
51+
/// <typeparam name="TResult"></typeparam>
52+
/// <param name="responseTask"></param>
53+
/// <param name="queryOperationName"></param>
54+
/// <returns>Returns an IGraphQLQueryResults set of typed results.</returns>
55+
public static async Task<TResult> ReceiveGraphQLMutationResult<TResult>(this Task<IFlurlGraphQLResponse> responseTask, string queryOperationName = null)
56+
where TResult : class
57+
{
58+
return await responseTask.ProcessResponsePayloadInternalAsync((resultPayload, _) =>
59+
{
60+
var results = resultPayload.LoadTypedResults<TResult>(queryOperationName);
61+
//For Single Item Mutation Result, we process with the same logic but there will be only one item (vs many for a Query)...
62+
var mutationResult = results.FirstOrDefault();
63+
return mutationResult;
64+
}).ConfigureAwait(false);
65+
}
66+
67+
/// <summary>
68+
/// Processes/parses the results of the GraphQL mutation execution into the specified result payload.
69+
/// This assumes that the Mutation conventions used follow best practices in that GraphQL mutations should take in and Input payload
70+
/// and return a result Payload; the result payload may be the single object type or a root type for a collection of results & errors (as defined by the GraphQL Schema).
71+
/// However, if you need more control over the processing of the Results you can dynamically handle it with the ReceiveGraphQLRawJsonResponse() method.
72+
/// </summary>
73+
/// <typeparam name="TResult"></typeparam>
74+
/// <param name="response"></param>
75+
/// <param name="queryOperationName"></param>
76+
/// <returns>Returns an IGraphQLQueryResults set of typed results.</returns>
77+
public static Task<TResult> ReceiveGraphQLMutationResult<TResult>(this IFlurlGraphQLResponse response, string queryOperationName = null)
78+
where TResult : class => Task.FromResult(response).ReceiveGraphQLMutationResult<TResult>(queryOperationName);
79+
80+
4481
/// <summary>
4582
/// Processes/parses the results of the GraphQL query execution into a raw Json Result with all raw Json response Data available for processing.
4683
/// </summary>

FlurlGraphQL.Querying/Flurl/InternalClasses/FlurlGraphQLResponsePayload.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ public IGraphQLQueryResults<TResult> LoadTypedResults<TResult>(string queryOpera
3939
: null;
4040

4141
var typedResults = querySingleResultJson.ParseJsonToGraphQLResultsInternal<TResult>(jsonSerializerSettings);
42+
43+
if(typedResults is GraphQLQueryResults<TResult> graphqlResults)
44+
graphqlResults.SetErrorsInternal(Errors);
45+
4246
return typedResults;
4347
}
4448
}

FlurlGraphQL.Querying/FlurlGraphQL.Querying.csproj

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@
22

33
<PropertyGroup>
44
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
5-
<Version>1.2.0</Version>
6-
<AssemblyVersion>1.2.0</AssemblyVersion>
7-
<FileVersion>1.2.0</FileVersion>
5+
<Version>1.3.0</Version>
6+
<AssemblyVersion>1.3.0</AssemblyVersion>
7+
<FileVersion>1.3.0</FileVersion>
88
<Authors>BBernard / CajunCoding</Authors>
99
<Company>CajunCoding</Company>
10-
<Description>GraphQL client querying extensions for Flurl.Http -- lightweight, simplified, asynchronous, fluent GraphQL client querying API extensions for the amazing Flurl Http library!</Description>
10+
<Description>GraphQL client extensions for Flurl.Http -- lightweight, simplified, asynchronous, fluent GraphQL client API extensions for the amazing Flurl Http library!</Description>
1111
<Copyright>Copyright © 2023</Copyright>
1212
<PackageLicenseExpression>MIT</PackageLicenseExpression>
1313
<PackageProjectUrl>https://github.com/cajuncoding/FlurlGraphQL</PackageProjectUrl>
1414
<RepositoryUrl>https://github.com/cajuncoding/FlurlGraphQL</RepositoryUrl>
1515
<PackageReleaseNotes>
1616
Release Notes:
17-
- Added support to control the Persisted Query payload field name for other GraphQL servers (e.g. Relay server) which may be different than HotChocolate .NET GraphQL Server.
18-
- Added global configuration support via FlurlGraphQLConfig.ConfigureDefaults(config => ...) so that configurable options can be set once globlly with current support for Persisted Query Field Name and Json Serializer Settings.
17+
- Added better support for Mutation handling so that single payload (per Mutation convention best practices) can be returned easily via ReceiveGraphQLMutationResult;
18+
this eliminates the need to use ReceiveGraphQLRawJsonResponse for dynamic Mutation response handling.
19+
- Fixed bug to ensure Errors are returned on IGraphQLQueryResults when possible (not available on Batch Queries).
20+
- Fixed bug in processing logic for paginated reqquests when TotalCount is the only selected field on a paginated request; only affected CollectionSegment/Offset Paging requests.
1921

2022
Prior Release Notes:
23+
- Added support to control the Persisted Query payload field name for other GraphQL servers (e.g. Relay server) which may be different than HotChocolate .NET GraphQL Server.
24+
- Added global configuration support via FlurlGraphQLConfig.ConfigureDefaults(config => ...) so that configurable options can be set once globlly with current support for Persisted Query Field Name and Json Serializer Settings.
2125
- Added support for Persisted Queries via .WithGraphQLPersistedQuery() api.
2226
- Added support to execute GraphQL as GET requests for edge cases, though POST requests are highly encouraged.
2327
- Improved consistency of use of (optional) custom Json Serialization settings when SetGraphQLNewtonsoftJsonSerializerSettings() is used to override the default GraphQL settings;

FlurlGraphQL.Querying/GraphQL/GraphQLQueryResults.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace FlurlGraphQL.Querying
1010
public class GraphQLQueryResults<TResult> : IReadOnlyList<TResult>, IGraphQLQueryResults<TResult>
1111
where TResult : class
1212
{
13-
public GraphQLQueryResults(IList<TResult> results = null, IList<GraphQLError> errors = null)
13+
public GraphQLQueryResults(IList<TResult> results = null, IReadOnlyList<GraphQLError> errors = null)
1414
{
1515
//The Results should be null safe as an empty list if no results exist.
1616
Results = results ?? new List<TResult>();
@@ -19,12 +19,19 @@ public GraphQLQueryResults(IList<TResult> results = null, IList<GraphQLError> er
1919
Errors = errors;
2020
}
2121

22+
//NOTE: Due to various code flows and inheritance it's not easy (or clean) to pass the Errors through
23+
// to the constructor so to simplify we have support to Internally Set this value when building the Results...
24+
internal void SetErrorsInternal(IReadOnlyList<GraphQLError> errors)
25+
{
26+
Errors = errors;
27+
}
28+
2229
protected IList<TResult> Results { get; }
2330
public bool HasAnyResults() => Results.Count > 0;
2431
// Internal Helper for Constructing results (e.g. GraphQLCollectionSegmentResults paging which is adapted from the Connection Results)...
2532
internal IList<TResult> GetResultsInternal() => Results;
2633

27-
public IList<GraphQLError> Errors { get; }
34+
public IReadOnlyList<GraphQLError> Errors { get; protected set; }
2835
public bool HasAnyErrors() => this.Errors?.Count > 0;
2936

3037
#region IReadOnlyList / IEnumerable Implementation

FlurlGraphQL.Querying/GraphQL/Interfaces/IGraphQLQueryResults.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public interface IGraphQLQueryResults<out TResult> : IReadOnlyList<TResult>
66
where TResult: class
77
{
88
bool HasAnyResults();
9-
IList<GraphQLError> Errors { get; }
9+
IReadOnlyList<GraphQLError> Errors { get; }
1010
bool HasAnyErrors();
1111
}
1212

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,6 @@ foreach (var pageTask in graphqlPagesTasks)
260260
}
261261
```
262262

263-
264263
### Offset/Slice Paging results via CollectionSegment...
265264
Offset based paging is not recommeded by GraphQL.org and therefore is less formalized with [no recommended implemenation](https://graphql.org/learn/pagination/#pagination-and-edges).
266265
So this api is compliant with the [HotChocolate .NET GraphQL Server] approach wich is fully GraphQL Spec compliant and [provides a formal implementaiton of Offset paging](https://chillicream.com/docs/hotchocolate/v12/fetching-data/pagination#offset-pagination)
@@ -389,6 +388,49 @@ foreach (var result in results)
389388
}
390389
```
391390

391+
### Mutations...
392+
GraphQL provides the ability to create, update, delete data in what are called mutation operations. In addition, *mutations* are different from queries in that they
393+
have different conventions and best practices for their implementations. In general, GraphQL mutations should take in a single *Input* and return a single
394+
result *Payload*; the result payload may be a single business object type but is more commonly a root type for a collection of Results & Errors (as defined by the GraphQL Schema).
395+
396+
Due to the complexity of all the varying types of Mutation Input & result Paylaod designs the API for mutations will fallback to parse the response as a single
397+
object result model (as opposed to a an Array that a Query would return). Therefore, your model should implement any/all of the response Payload field features you are interested in.
398+
399+
And if you need even more low level processing or just want to handle the Mutation result more dynamically then you can always use raw Json handling
400+
via the ReceiveGraphQLRawJsonResponse API (see below).
401+
402+
```csharp
403+
var newCharacterModel = new CharacterModel()
404+
{
405+
//...Populate the new Character Model...
406+
};
407+
408+
var json = await "https://graphql-star-wars.azurewebsites.net/api/graphql"
409+
.WithGraphQLQuery(@"
410+
mutation ($newCharacter: Character) {
411+
characterCreateOrUpdate(input: $newCharacter) {
412+
result {
413+
personalIdentifier
414+
name
415+
}
416+
errors {
417+
... on Error {
418+
errorCode
419+
message
420+
}
421+
}
422+
}
423+
}
424+
")
425+
.SetGraphQLVariables(new { newCharacter: newCharacterModel })
426+
.PostGraphQLQueryAsync()
427+
//NOTE: Here CharacterCreateOrUpdate Result will a single Payload result (vs a List as Query would return)
428+
// for which teh model would have both a Result property & an Errors property to be deserialized based
429+
// on the unique GraphQL Schema Mutation design...
430+
.ReceiveGraphQLMutationResult<CharacterCreateOrUpdateResult>();
431+
```
432+
433+
392434
### Batch Querying...
393435
GraphQL provides the ability to execute multiple queries in a single request as a batch. When this is done each response is provided in the same order as requested
394436
in the json response object named the same as the GraphQL Query operation. The api provides the ability to retrieve all results (vs only the first by default) by

0 commit comments

Comments
 (0)