Skip to content

Commit

Permalink
move code for generation of the APQ extension into GraphQLRequest
Browse files Browse the repository at this point in the history
  • Loading branch information
rose-a committed Apr 25, 2024
1 parent 77a6d7e commit 0b47264
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 36 deletions.
4 changes: 0 additions & 4 deletions src/GraphQL.Client.Abstractions/GraphQLClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ public static Task<GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(this IG
cancellationToken: cancellationToken);
}

#if NET6_0_OR_GREATER
public static Task<GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(this IGraphQLClient client,
GraphQLQuery query, object? variables = null,
string? operationName = null, Func<TResponse>? defineResponseType = null,
CancellationToken cancellationToken = default)
=> SendQueryAsync(client, query.Text, variables, operationName, defineResponseType,
cancellationToken);
#endif

public static Task<GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(this IGraphQLClient client,
[StringSyntax("GraphQL")] string query, object? variables = null,
Expand All @@ -31,13 +29,11 @@ public static Task<GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(this
cancellationToken: cancellationToken);
}

#if NET6_0_OR_GREATER
public static Task<GraphQLResponse<TResponse>> SendMutationAsync<TResponse>(this IGraphQLClient client,
GraphQLQuery query, object? variables = null, string? operationName = null, Func<TResponse>? defineResponseType = null,
CancellationToken cancellationToken = default)
=> SendMutationAsync(client, query.Text, variables, operationName, defineResponseType,
cancellationToken);
#endif

public static Task<GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(this IGraphQLClient client,
GraphQLRequest request, Func<TResponse> defineResponseType, CancellationToken cancellationToken = default)
Expand Down
4 changes: 0 additions & 4 deletions src/GraphQL.Client/GraphQL.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@
<PackageReference Include="System.Net.WebSockets.Client.Managed" Version="1.0.22" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Memory" Version="4.5.5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\GraphQL.Client.Abstractions.Websocket\GraphQL.Client.Abstractions.Websocket.csproj" />
<ProjectReference Include="..\GraphQL.Client.Abstractions\GraphQL.Client.Abstractions.csproj" />
Expand Down
10 changes: 3 additions & 7 deletions src/GraphQL.Client/GraphQLHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,15 @@ public async Task<GraphQLResponse<TResponse>> SendQueryAsync<TResponse>(GraphQLR
{
cancellationToken.ThrowIfCancellationRequested();

string? savedQuery = request.Query;
string? savedQuery = null;
bool useAPQ = false;

if (request.Query != null && !APQDisabledForSession && Options.EnableAutomaticPersistedQueries(request))
{
// https://www.apollographql.com/docs/react/api/link/persisted-queries/
useAPQ = true;
request.Extensions ??= new();
request.Extensions["persistedQuery"] = new Dictionary<string, object>
{
["version"] = APQ_SUPPORTED_VERSION,
["sha256Hash"] = Hash.Compute(request.Query),
};
request.GeneratePersistedQueryExtension();
savedQuery = request.Query;
request.Query = null;
}

Expand Down
3 changes: 0 additions & 3 deletions src/GraphQL.Client/GraphQLHttpRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,10 @@ public GraphQLHttpRequest([StringSyntax("GraphQL")] string query, object? variab
: base(query, variables, operationName, extensions)
{
}

#if NET6_0_OR_GREATER
public GraphQLHttpRequest(GraphQLQuery query, object? variables = null, string? operationName = null, Dictionary<string, object?>? extensions = null)
: base(query, variables, operationName, extensions)
{
}
#endif

public GraphQLHttpRequest(GraphQLRequest other)
: base(other)
Expand Down
3 changes: 3 additions & 0 deletions src/GraphQL.Primitives/GraphQL.Primitives.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@
<TargetFrameworks>netstandard2.0;net6.0;net7.0;net8.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Memory" Version="4.5.5" />
</ItemGroup>
</Project>
33 changes: 26 additions & 7 deletions src/GraphQL.Primitives/GraphQLQuery.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
#if NET6_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;

namespace GraphQL;

/// <summary>
/// Value record for a GraphQL query string
/// Value object representing a GraphQL query string and storing the corresponding APQ hash. <br />
/// Use this to hold query strings you want to use more than once.
/// </summary>
/// <param name="Text">the actual query string</param>
public readonly record struct GraphQLQuery([StringSyntax("GraphQL")] string Text)
public class GraphQLQuery : IEquatable<GraphQLQuery>
{
/// <summary>
/// The actual query string
/// </summary>
public string Text { get; }

/// <summary>
/// The SHA256 hash used for the advanced persisted queries feature (APQ)
/// </summary>
public string Sha256Hash { get; }

public GraphQLQuery([StringSyntax("GraphQL")] string text)
{
Text = text;
Sha256Hash = Hash.Compute(Text);
}

public static implicit operator string(GraphQLQuery query)
=> query.Text;
};
#endif

public bool Equals(GraphQLQuery other) => Sha256Hash == other.Sha256Hash;

public override bool Equals(object? obj) => obj is GraphQLQuery other && Equals(other);

public override int GetHashCode() => Sha256Hash.GetHashCode();
}
32 changes: 27 additions & 5 deletions src/GraphQL.Primitives/GraphQLRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,29 @@ public class GraphQLRequest : Dictionary<string, object>, IEquatable<GraphQLRequ
public const string QUERY_KEY = "query";
public const string VARIABLES_KEY = "variables";
public const string EXTENSIONS_KEY = "extensions";
public const string EXTENSIONS_PERSISTED_QUERY_KEY = "persistedQuery";
public const int APQ_SUPPORTED_VERSION = 1;

private string? _sha265Hash;

/// <summary>
/// The Query
/// The query string
/// </summary>
[StringSyntax("GraphQL")]
public string? Query
{
get => TryGetValue(QUERY_KEY, out object value) ? (string)value : null;
set => this[QUERY_KEY] = value;
set
{
this[QUERY_KEY] = value;
// if the query string gets overwritten, reset the hash value
if (_sha265Hash is not null)
_sha265Hash = null;
}
}

/// <summary>
/// The name of the Operation
/// The operation to execute
/// </summary>
public string? OperationName
{
Expand Down Expand Up @@ -59,16 +69,28 @@ public GraphQLRequest([StringSyntax("GraphQL")] string query, object? variables
Extensions = extensions;
}

#if NET6_0_OR_GREATER
public GraphQLRequest(GraphQLQuery query, object? variables = null, string? operationName = null,
Dictionary<string, object?>? extensions = null)
: this(query.Text, variables, operationName, extensions)
{
_sha265Hash = query.Sha256Hash;
}
#endif

public GraphQLRequest(GraphQLRequest other) : base(other) { }

public void GeneratePersistedQueryExtension()
{
if (Query is null)
throw new InvalidOperationException($"{nameof(Query)} is null");

Extensions ??= new();
Extensions[EXTENSIONS_PERSISTED_QUERY_KEY] = new Dictionary<string, object>
{
["version"] = APQ_SUPPORTED_VERSION,
["sha256Hash"] = _sha265Hash ?? Hash.Compute(Query),
};
}

/// <summary>
/// Returns a value that indicates whether this instance is equal to a specified object
/// </summary>
Expand Down
12 changes: 6 additions & 6 deletions src/GraphQL.Client/Hash.cs → src/GraphQL.Primitives/Hash.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
using System.Security.Cryptography;
using System.Text;

namespace GraphQL.Client.Http;
namespace GraphQL;

internal static class Hash
{
private static SHA256? _sha256;

internal static string Compute(string query)
{
var expected = Encoding.UTF8.GetByteCount(query);
var inputBytes = ArrayPool<byte>.Shared.Rent(expected);
var written = Encoding.UTF8.GetBytes(query, 0, query.Length, inputBytes, 0);
Debug.Assert(written == expected, $"Encoding.UTF8.GetBytes returned unexpected bytes: {written} instead of {expected}");
int expected = Encoding.UTF8.GetByteCount(query);
byte[]? inputBytes = ArrayPool<byte>.Shared.Rent(expected);
int written = Encoding.UTF8.GetBytes(query, 0, query.Length, inputBytes, 0);
Debug.Assert(written == expected, (string)$"Encoding.UTF8.GetBytes returned unexpected bytes: {written} instead of {expected}");

var shaShared = Interlocked.Exchange(ref _sha256, null) ?? SHA256.Create();

Expand All @@ -33,7 +33,7 @@ internal static string Compute(string query)
return Convert.ToHexString(bytes);
#else
var builder = new StringBuilder(bytes.Length * 2);
foreach (var item in bytes)
foreach (byte item in bytes)
{
builder.Append(item.ToString("x2"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,33 @@ query Human($id: String!){
Assert.Equal(name, response.Data.Human.Name);
StarWarsWebsocketClient.APQDisabledForSession.Should().BeFalse("if APQ has worked it won't get disabled");
}

[Fact]
public void Verify_the_persisted_query_extension_object()
{
var query = new GraphQLQuery("""
query Human($id: String!){
human(id: $id) {
name
}
}
""");
query.Sha256Hash.Should().NotBeNullOrEmpty();

var request = new GraphQLRequest(query);
request.Extensions.Should().BeNull();
request.GeneratePersistedQueryExtension();
request.Extensions.Should().NotBeNull();

string expectedKey = "persistedQuery";
var expectedExtensionValue = new Dictionary<string, object>
{
["version"] = 1,
["sha256Hash"] = query.Sha256Hash,
};

request.Extensions.Should().ContainKey(expectedKey);
request.Extensions![expectedKey].As<Dictionary<string, object>>()
.Should().NotBeNull().And.BeEquivalentTo(expectedExtensionValue);
}
}

0 comments on commit 0b47264

Please sign in to comment.