Refactor MaybeThrowGraphqlException to throw AggregateException with all GraphQL errors (#169)
* first pass * Updated tests * Removed path from message
This commit is contained in:
@@ -5,15 +5,15 @@ namespace Speckle.Sdk.Dependencies;
|
||||
|
||||
public static class GraphQLRetry
|
||||
{
|
||||
public static async Task<T> ExecuteAsync<T, TException>(
|
||||
public static async Task<T> ExecuteAsync<T, TInnerException>(
|
||||
Func<Task<T>> func,
|
||||
Action<Exception, TimeSpan>? onRetry = null
|
||||
)
|
||||
where TException : Exception
|
||||
where TInnerException : Exception
|
||||
{
|
||||
var delay = Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), 5);
|
||||
var graphqlRetry = Policy
|
||||
.Handle<TException>()
|
||||
.HandleInner<TInnerException>()
|
||||
.WaitAndRetryAsync(
|
||||
delay,
|
||||
(ex, timeout, _) =>
|
||||
|
||||
@@ -1,53 +1,17 @@
|
||||
using GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
|
||||
namespace Speckle.Sdk.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for GraphQL API exceptions
|
||||
/// The base class for all GraphQL errors (these are errors in the graphql response)
|
||||
/// Some specific codes are maped to subtypes <see cref="GraphQLErrorHandler"/>
|
||||
/// <seealso cref="SpeckleGraphQLForbiddenException"/>
|
||||
/// <seealso cref="SpeckleGraphQLInternalErrorException"/>
|
||||
/// <seealso cref="SpeckleGraphQLBadInputException"/>
|
||||
/// <seealso cref="SpeckleGraphQLInvalidQueryException"/>
|
||||
/// </summary>
|
||||
public class SpeckleGraphQLException<T> : SpeckleGraphQLException
|
||||
{
|
||||
public new GraphQLResponse<T>? Response => (GraphQLResponse<T>?)base.Response;
|
||||
|
||||
public SpeckleGraphQLException(
|
||||
string message,
|
||||
GraphQLRequest request,
|
||||
GraphQLResponse<T>? response,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base(message, request, response, innerException) { }
|
||||
|
||||
public SpeckleGraphQLException() { }
|
||||
|
||||
public SpeckleGraphQLException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public SpeckleGraphQLException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class SpeckleGraphQLException : SpeckleException
|
||||
{
|
||||
private readonly GraphQLRequest _request;
|
||||
public IGraphQLResponse? Response { get; }
|
||||
|
||||
public IEnumerable<string> ErrorMessages =>
|
||||
Response?.Errors != null ? Response.Errors.Select(e => e.Message) : Enumerable.Empty<string>();
|
||||
|
||||
public IDictionary<string, object>? Extensions => Response?.Extensions;
|
||||
|
||||
public SpeckleGraphQLException(
|
||||
string? message,
|
||||
GraphQLRequest request,
|
||||
IGraphQLResponse? response,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base(message, innerException)
|
||||
{
|
||||
_request = request;
|
||||
Response = response;
|
||||
}
|
||||
|
||||
public SpeckleGraphQLException() { }
|
||||
|
||||
public SpeckleGraphQLException(string? message)
|
||||
@@ -58,19 +22,12 @@ public class SpeckleGraphQLException : SpeckleException
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a "FORBIDDEN" on "UNAUTHORIZED" GraphQL error as an exception.
|
||||
/// Represents a "FORBIDDEN" or "UNAUTHORIZED" GraphQL error as an exception.
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#unauthenticated
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors/#forbidden
|
||||
/// </summary>
|
||||
public class SpeckleGraphQLForbiddenException : SpeckleGraphQLException
|
||||
public sealed class SpeckleGraphQLForbiddenException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLForbiddenException(
|
||||
GraphQLRequest request,
|
||||
IGraphQLResponse response,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base("Your request was forbidden", request, response, innerException) { }
|
||||
|
||||
public SpeckleGraphQLForbiddenException() { }
|
||||
|
||||
public SpeckleGraphQLForbiddenException(string? message)
|
||||
@@ -80,15 +37,12 @@ public class SpeckleGraphQLForbiddenException : SpeckleGraphQLException
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class SpeckleGraphQLInternalErrorException : SpeckleGraphQLException
|
||||
/// <summary>
|
||||
/// Represents a "INTERNAL_SERVER_ERROR" GraphQL error as an exception.
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors#internal_server_error
|
||||
/// </summary>
|
||||
public sealed class SpeckleGraphQLInternalErrorException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLInternalErrorException(
|
||||
GraphQLRequest request,
|
||||
IGraphQLResponse response,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base("Your request failed on the server side", request, response, innerException) { }
|
||||
|
||||
public SpeckleGraphQLInternalErrorException() { }
|
||||
|
||||
public SpeckleGraphQLInternalErrorException(string? message)
|
||||
@@ -98,15 +52,11 @@ public class SpeckleGraphQLInternalErrorException : SpeckleGraphQLException
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
public class SpeckleGraphQLStreamNotFoundException : SpeckleGraphQLException
|
||||
/// <summary>
|
||||
/// Represents the custom "STREAM_NOT_FOUND" GraphQL error as an exception.
|
||||
/// </summary>
|
||||
public sealed class SpeckleGraphQLStreamNotFoundException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLStreamNotFoundException(
|
||||
GraphQLRequest request,
|
||||
IGraphQLResponse response,
|
||||
Exception? innerException = null
|
||||
)
|
||||
: base("Stream not found", request, response, innerException) { }
|
||||
|
||||
public SpeckleGraphQLStreamNotFoundException() { }
|
||||
|
||||
public SpeckleGraphQLStreamNotFoundException(string? message)
|
||||
@@ -115,3 +65,34 @@ public class SpeckleGraphQLStreamNotFoundException : SpeckleGraphQLException
|
||||
public SpeckleGraphQLStreamNotFoundException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a "BAD_USER_INPUT" GraphQL error as an exception.
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors#bad_user_input
|
||||
/// </summary>
|
||||
public sealed class SpeckleGraphQLBadInputException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLBadInputException() { }
|
||||
|
||||
public SpeckleGraphQLBadInputException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public SpeckleGraphQLBadInputException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a "GRAPHQL_PARSE_FAILED" or "GRAPHQL_VALIDATION_FAILED" GraphQL error as an exception.
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors#graphql_parse_failed
|
||||
/// https://www.apollographql.com/docs/apollo-server/v2/data/errors#graphql_validation_failed
|
||||
/// </summary>
|
||||
public sealed class SpeckleGraphQLInvalidQueryException : SpeckleGraphQLException
|
||||
{
|
||||
public SpeckleGraphQLInvalidQueryException() { }
|
||||
|
||||
public SpeckleGraphQLInvalidQueryException(string? message)
|
||||
: base(message) { }
|
||||
|
||||
public SpeckleGraphQLInvalidQueryException(string? message, Exception? innerException)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ public sealed class Client : ISpeckleGraphQLClient, IDisposable
|
||||
GraphQLResponse<T> result = await GQLClient
|
||||
.SendMutationAsync<T>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
MaybeThrowFromGraphQLErrors(request, result);
|
||||
result.EnsureGraphQLSuccess();
|
||||
return result.Data;
|
||||
})
|
||||
.ConfigureAwait(false);
|
||||
@@ -132,129 +132,48 @@ public sealed class Client : ISpeckleGraphQLClient, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
internal void MaybeThrowFromGraphQLErrors<T>(GraphQLRequest request, GraphQLResponse<T> response)
|
||||
{
|
||||
// The errors reflect the Apollo server v2 API, which is deprecated. It is bound to change,
|
||||
// once we migrate to a newer version.
|
||||
var errors = response.Errors;
|
||||
if (errors != null && errors.Length != 0)
|
||||
{
|
||||
if (
|
||||
errors.Any(e =>
|
||||
e.Extensions != null
|
||||
&& (
|
||||
e.Extensions.Contains(new KeyValuePair<string, object>("code", "FORBIDDEN"))
|
||||
|| e.Extensions.Contains(new KeyValuePair<string, object>("code", "UNAUTHENTICATED"))
|
||||
)
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new SpeckleGraphQLForbiddenException(request, response);
|
||||
}
|
||||
|
||||
if (
|
||||
errors.Any(e =>
|
||||
e.Extensions != null && e.Extensions.Contains(new KeyValuePair<string, object>("code", "STREAM_NOT_FOUND"))
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new SpeckleGraphQLStreamNotFoundException(request, response);
|
||||
}
|
||||
|
||||
if (
|
||||
errors.Any(e =>
|
||||
e.Extensions != null
|
||||
&& e.Extensions.Contains(new KeyValuePair<string, object>("code", "INTERNAL_SERVER_ERROR"))
|
||||
)
|
||||
)
|
||||
{
|
||||
throw new SpeckleGraphQLInternalErrorException(request, response);
|
||||
}
|
||||
|
||||
throw new SpeckleGraphQLException<T>("Request failed with errors", request, response);
|
||||
}
|
||||
}
|
||||
|
||||
IDisposable ISpeckleGraphQLClient.SubscribeTo<T>(GraphQLRequest request, Action<object, T> callback) =>
|
||||
SubscribeTo(request, callback);
|
||||
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.SubscribeTo{T}"/>
|
||||
private IDisposable SubscribeTo<T>(GraphQLRequest request, Action<object, T> callback)
|
||||
{
|
||||
//using (LogContext.Push(CreateEnrichers<T>(request)))
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var res = GQLClient.CreateSubscriptionStream<T>(request);
|
||||
return res.Subscribe(
|
||||
response =>
|
||||
var res = GQLClient.CreateSubscriptionStream<T>(request);
|
||||
return res.Subscribe(
|
||||
response =>
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
MaybeThrowFromGraphQLErrors(request, response);
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
if (response.Data != null)
|
||||
{
|
||||
callback(this, response.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Serilog.Log.ForContext("graphqlResponse", response)
|
||||
_logger.LogError(
|
||||
"Cannot execute graphql callback for {resultType}, the response has no data.",
|
||||
typeof(T).Name
|
||||
);
|
||||
}
|
||||
}
|
||||
// we catch forbidden to rethrow, making sure its not logged.
|
||||
catch (SpeckleGraphQLForbiddenException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
// anything else related to graphql gets logged
|
||||
catch (SpeckleGraphQLException<T> gqlException)
|
||||
{
|
||||
/* Speckle.Sdk.Logging..ForContext("graphqlResponse", gqlException.Response)
|
||||
.ForContext("graphqlExtensions", gqlException.Extensions)
|
||||
.ForContext("graphqlErrorMessages", gqlException.ErrorMessages.ToList())*/
|
||||
_logger.LogWarning(
|
||||
gqlException,
|
||||
"Execution of the graphql request to get {resultType} failed with {graphqlExceptionType} {exceptionMessage}.",
|
||||
typeof(T).Name,
|
||||
gqlException.GetType().Name,
|
||||
gqlException.Message
|
||||
);
|
||||
throw;
|
||||
}
|
||||
// we're not handling the bare Exception type here,
|
||||
// since we have a response object on the callback, we know the Exceptions
|
||||
// can only be thrown from the MaybeThrowFromGraphQLErrors which wraps
|
||||
// every exception into SpeckleGraphQLException
|
||||
},
|
||||
ex =>
|
||||
{
|
||||
// we're logging this as an error for now, to keep track of failures
|
||||
// so far we've swallowed these errors
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Subscription for {resultType} terminated unexpectedly with {exceptionMessage}",
|
||||
typeof(T).Name,
|
||||
ex.Message
|
||||
);
|
||||
// we could be throwing like this:
|
||||
// throw ex;
|
||||
callback(this, response.Data);
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
{
|
||||
throw new SpeckleGraphQLException<T>(
|
||||
"The graphql request failed without a graphql response",
|
||||
request,
|
||||
null,
|
||||
ex
|
||||
);
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Subscription for {type} got a response with errors", typeof(T).Name);
|
||||
throw;
|
||||
}
|
||||
},
|
||||
ex =>
|
||||
{
|
||||
// we're logging this as an error for now, to keep track of failures
|
||||
// so far we've swallowed these errors
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Subscription for {resultType} terminated unexpectedly with {exceptionMessage}",
|
||||
typeof(T).Name,
|
||||
ex.Message
|
||||
);
|
||||
// we could be throwing like this:
|
||||
// throw ex;
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal() && ex is not ObjectDisposedException)
|
||||
{
|
||||
throw new SpeckleGraphQLException($"Subscription for {typeof(T)} failed to start", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using GraphQL;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL;
|
||||
|
||||
internal static class GraphQLErrorHandler
|
||||
{
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="EnsureGraphQLSuccess(IReadOnlyCollection{GraphQLError}?)"/></exception>
|
||||
public static void EnsureGraphQLSuccess(this IGraphQLResponse response) => EnsureGraphQLSuccess(response.Errors);
|
||||
|
||||
/// <exception cref="AggregateException">Containing a <see cref="SpeckleGraphQLException"/> (or subclass of) for each graphql Error</exception>
|
||||
public static void EnsureGraphQLSuccess(IReadOnlyCollection<GraphQLError>? errors)
|
||||
{
|
||||
// The errors reflect the Apollo server v2 API, which is deprecated. It is bound to change,
|
||||
// once we migrate to a newer version.
|
||||
if (errors == null || errors.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<SpeckleGraphQLException> exceptions = new(errors.Count);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
object? code = null;
|
||||
_ = error.Extensions?.TryGetValue("code", out code);
|
||||
|
||||
var message = FormatErrorMessage(error, code);
|
||||
var ex = code switch
|
||||
{
|
||||
"GRAPHQL_PARSE_FAILED" or "GRAPHQL_VALIDATION_FAILED" => new SpeckleGraphQLInvalidQueryException(message),
|
||||
"FORBIDDEN" or "UNAUTHENTICATED" => new SpeckleGraphQLForbiddenException(message),
|
||||
"STREAM_NOT_FOUND" => new SpeckleGraphQLStreamNotFoundException(message),
|
||||
"BAD_USER_INPUT" => new SpeckleGraphQLBadInputException(message),
|
||||
"INTERNAL_SERVER_ERROR" => new SpeckleGraphQLInternalErrorException(message),
|
||||
_ => new SpeckleGraphQLException(message),
|
||||
};
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
|
||||
throw new AggregateException("Request failed with GraphQL errors, see inner exceptions", exceptions);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
private static string FormatErrorMessage(GraphQLError error, object? code)
|
||||
{
|
||||
code ??= "ERROR";
|
||||
return $"{code}: {error.Message}";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using GraphQL;
|
||||
using GraphQL.Client.Http;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
using Speckle.Sdk.Common;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL;
|
||||
|
||||
@@ -10,46 +9,51 @@ public static class GraphQLHttpClientExtensions
|
||||
/// <summary>
|
||||
/// Gets the version of the current server. Useful for guarding against unsupported api calls on newer or older servers.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">[Optional] defaults to an empty cancellation token</param>
|
||||
/// <returns><see cref="Version"/> object excluding any strings (eg "2.7.2-alpha.6995" becomes "2.7.2.6995")</returns>
|
||||
/// <exception cref="SpeckleGraphQLException{ServerInfoResponse}"></exception>
|
||||
/// <remarks>
|
||||
/// Expects the response to either be<br/>
|
||||
/// - 1. The literal string <c>dev</c>, which will return <c>999.999.999</c><br/>
|
||||
/// - 2. A 3 numeral semver (anything after the first <c>-</c> character will be ignored)<br/>
|
||||
/// </remarks>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>A 3 numeral <see cref="Version"/> object (e.g. <c>2.21.3.alpha123</c> becomes <c>2.21.3</c>)</returns>
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
/// <exception cref="FormatException">Server responded with a server version, but it was not in an expected format</exception>
|
||||
public static async Task<System.Version> GetServerVersion(
|
||||
this GraphQLHttpClient client,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var request = new GraphQLRequest
|
||||
{
|
||||
Query =
|
||||
@"query Server {
|
||||
serverInfo {
|
||||
version
|
||||
}
|
||||
}",
|
||||
};
|
||||
//lang=graphql
|
||||
const string QUERY = """
|
||||
query Server {
|
||||
data:serverInfo {
|
||||
data:version
|
||||
}
|
||||
}
|
||||
""";
|
||||
var request = new GraphQLRequest { Query = QUERY };
|
||||
|
||||
var response = await client.SendQueryAsync<ServerInfoResponse>(request, cancellationToken).ConfigureAwait(false);
|
||||
var response = await client
|
||||
.SendQueryAsync<RequiredResponse<RequiredResponse<string>>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.Errors != null)
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
string versionString = response.Data.data.data;
|
||||
if (versionString == "dev")
|
||||
{
|
||||
throw new SpeckleGraphQLException<ServerInfoResponse>(
|
||||
$"Query {nameof(GetServerVersion)} failed",
|
||||
request,
|
||||
response
|
||||
);
|
||||
return new Version(999, 999, 999);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(response.Data.serverInfo.version))
|
||||
{
|
||||
throw new SpeckleGraphQLException<ServerInfoResponse>(
|
||||
$"Query {nameof(GetServerVersion)} did not provide a valid server version",
|
||||
request,
|
||||
response
|
||||
);
|
||||
}
|
||||
string? semverString = versionString.Split('-').First();
|
||||
|
||||
return response.Data.serverInfo.version == "dev"
|
||||
? new System.Version(999, 999, 999)
|
||||
: new System.Version(response.Data.serverInfo.version.NotNull().Split('-').First());
|
||||
if (Version.TryParse(semverString!, out Version? semver))
|
||||
{
|
||||
return semver;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FormatException($"Server responded with an invalid semver string \"{semverString}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
using GraphQL;
|
||||
using GraphQL.Client.Http;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
|
||||
namespace Speckle.Sdk.Api.GraphQL;
|
||||
|
||||
internal interface ISpeckleGraphQLClient
|
||||
{
|
||||
/// <exception cref="SpeckleGraphQLForbiddenException">"FORBIDDEN" on "UNAUTHORIZED" response from server</exception>
|
||||
/// <exception cref="SpeckleGraphQLException">All other request errors</exception>
|
||||
/// <exception cref="AggregateException">Request failed on the GraphQL layer, each GraphQL error will be a <see cref="SpeckleGraphQLException"/> (or subclass of) as an inner exception</exception>
|
||||
/// <exception cref="GraphQLHttpRequestException">Request failed on the HTTP layer (non-successful response code)</exception>
|
||||
/// <exception cref="HttpRequestException">The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout</exception>
|
||||
/// <exception cref="OperationCanceledException">The <paramref name="cancellationToken"/> requested a cancel</exception>
|
||||
/// <exception cref="ObjectDisposedException">This <see cref="Client"/> already been disposed</exception>
|
||||
/// <exception cref="JsonSerializationException">The response failed to deserialize, probably because the server version is incompatible with this version of the SDK, or there is a mistake in a query (queried for a property that isn't in the C# model, or a required property was null)</exception>
|
||||
/// <exception cref="JsonException">The response failed to deserialize, probably because the server version is incompatible with this version of the SDK, or there is a mistake in a query (queried for a property that isn't in the C# model, or a required property was null)</exception>
|
||||
internal Task<T> ExecuteGraphQLRequest<T>(GraphQLRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <exception cref="SpeckleGraphQLForbiddenException">"FORBIDDEN" on "UNAUTHORIZED" response from server</exception>
|
||||
/// <exception cref="SpeckleGraphQLException">All other request errors</exception>
|
||||
/// <exception cref="AggregateException">Containing a <see cref="SpeckleGraphQLException"/> (or subclass of) for each graphql Error</exception>
|
||||
/// <exception cref="ObjectDisposedException">This <see cref="Client"/> already been disposed</exception>
|
||||
internal IDisposable SubscribeTo<T>(GraphQLRequest request, Action<object, T> callback);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ public sealed class ActiveUserResource
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ISpeckleGraphQLClient.ExecuteGraphQLRequest{T}"/>
|
||||
/// <exception cref="SpeckleException">The ActiveUser could not be found (e.g. the client is not authenticated)</exception>
|
||||
public async Task<List<PendingStreamCollaborator>> GetProjectInvites(CancellationToken cancellationToken = default)
|
||||
{
|
||||
//language=graphql
|
||||
@@ -192,7 +193,7 @@ public sealed class ActiveUserResource
|
||||
|
||||
if (response.data is null)
|
||||
{
|
||||
throw new SpeckleGraphQLException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
throw new SpeckleException("GraphQL response indicated that the ActiveUser could not be found");
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: https://app.speckle.systems/graphql
|
||||
documents: '**/*.graphql'
|
||||
@@ -5,6 +5,7 @@ namespace Speckle.Sdk.Common;
|
||||
|
||||
public static class NotNullExtensions
|
||||
{
|
||||
/// <exception cref="ArgumentNullException">Thrown when the awaited <paramref name="task"/> returns <see langword="null"/></exception>
|
||||
public static async ValueTask<T> NotNull<T>(
|
||||
this ValueTask<T?> task,
|
||||
[CallerArgumentExpression(nameof(task))] string? message = null
|
||||
@@ -19,6 +20,7 @@ public static class NotNullExtensions
|
||||
return x;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NotNull{T}(System.Threading.Tasks.ValueTask{T?},string?)"/>
|
||||
public static async ValueTask<T> NotNull<T>(
|
||||
this ValueTask<T?> task,
|
||||
[CallerArgumentExpression(nameof(task))] string? message = null
|
||||
@@ -33,6 +35,7 @@ public static class NotNullExtensions
|
||||
return x.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NotNull{T}(System.Threading.Tasks.ValueTask{T?},string?)"/>
|
||||
public static async Task<T> NotNull<T>(
|
||||
this Task<T?> task,
|
||||
[CallerArgumentExpression(nameof(task))] string? message = null
|
||||
@@ -47,6 +50,7 @@ public static class NotNullExtensions
|
||||
return x;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NotNull{T}(System.Threading.Tasks.ValueTask{T?},string?)"/>
|
||||
public static async Task<T> NotNull<T>(
|
||||
this Task<T?> task,
|
||||
[CallerArgumentExpression(nameof(task))] string? message = null
|
||||
@@ -61,6 +65,7 @@ public static class NotNullExtensions
|
||||
return x.Value;
|
||||
}
|
||||
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="obj"/> is <see langword="null"/></exception>
|
||||
public static T NotNull<T>([NotNull] this T? obj, [CallerArgumentExpression(nameof(obj))] string? paramName = null)
|
||||
where T : class
|
||||
{
|
||||
@@ -71,6 +76,7 @@ public static class NotNullExtensions
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="NotNull{T}(T?,string?)"/>
|
||||
public static T NotNull<T>([NotNull] this T? obj, [CallerArgumentExpression(nameof(obj))] string? paramName = null)
|
||||
where T : struct
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@ using GraphQL.Client.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Speckle.InterfaceGenerator;
|
||||
using Speckle.Newtonsoft.Json;
|
||||
using Speckle.Sdk.Api;
|
||||
using Speckle.Sdk.Api.GraphQL;
|
||||
using Speckle.Sdk.Api.GraphQL.Models;
|
||||
using Speckle.Sdk.Api.GraphQL.Models.Responses;
|
||||
@@ -38,8 +37,10 @@ public class AccountManager(ISpeckleApplication application, ILogger<AccountMana
|
||||
/// <summary>
|
||||
/// Gets the basic information about a server.
|
||||
/// </summary>
|
||||
/// <param name="server">Server URL</param>
|
||||
/// <param name="server">Server Information</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="GraphQLHttpRequestException">Request failed on the HTTP layer (received a non-successful response code)</exception>
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
public async Task<ServerInfo> GetServerInfo(Uri server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var httpClient = speckleHttp.CreateHttpClient();
|
||||
@@ -78,14 +79,7 @@ public class AccountManager(ISpeckleApplication application, ILogger<AccountMana
|
||||
|
||||
var response = await gqlClient.SendQueryAsync<ServerInfoResponse>(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.Errors is not null)
|
||||
{
|
||||
throw new SpeckleGraphQLException<ServerInfoResponse>(
|
||||
$"GraphQL request {nameof(GetServerInfo)} failed",
|
||||
request,
|
||||
response
|
||||
);
|
||||
}
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
ServerInfo serverInfo = response.Data.serverInfo;
|
||||
serverInfo.url = server.ToString().TrimEnd('/');
|
||||
@@ -100,6 +94,8 @@ public class AccountManager(ISpeckleApplication application, ILogger<AccountMana
|
||||
/// <param name="token"></param>
|
||||
/// <param name="server">Server URL</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="GraphQLHttpRequestException">Request failed on the HTTP layer (received a non-successful response code)</exception>
|
||||
/// <exception cref="AggregateException"><inheritdoc cref="GraphQLErrorHandler.EnsureGraphQLSuccess(IGraphQLResponse)"/></exception>
|
||||
public async Task<UserInfo> GetUserInfo(string token, Uri server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var httpClient = speckleHttp.CreateHttpClient(authorizationToken: token);
|
||||
@@ -126,10 +122,7 @@ public class AccountManager(ISpeckleApplication application, ILogger<AccountMana
|
||||
.SendQueryAsync<RequiredResponse<UserInfo>>(request, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.Errors != null)
|
||||
{
|
||||
throw new SpeckleGraphQLException($"GraphQL request {nameof(GetUserInfo)} failed", request, response);
|
||||
}
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
return response.Data.data;
|
||||
}
|
||||
@@ -146,59 +139,45 @@ public class AccountManager(ISpeckleApplication application, ILogger<AccountMana
|
||||
CancellationToken ct = default
|
||||
)
|
||||
{
|
||||
try
|
||||
using var httpClient = speckleHttp.CreateHttpClient(authorizationToken: token);
|
||||
|
||||
using var client = new GraphQLHttpClient(
|
||||
new GraphQLHttpClientOptions { EndPoint = new Uri(server, "/graphql") },
|
||||
new NewtonsoftJsonSerializer(),
|
||||
httpClient
|
||||
);
|
||||
|
||||
System.Version version = await client.GetServerVersion(ct).ConfigureAwait(false);
|
||||
|
||||
// serverMigration property was added in 2.18.5, so only query for it
|
||||
// if the server has been updated past that version
|
||||
System.Version serverMigrationVersion = new(2, 18, 5);
|
||||
|
||||
string queryString;
|
||||
if (version >= serverMigrationVersion)
|
||||
{
|
||||
using var httpClient = speckleHttp.CreateHttpClient(authorizationToken: token);
|
||||
|
||||
using var client = new GraphQLHttpClient(
|
||||
new GraphQLHttpClientOptions { EndPoint = new Uri(server, "/graphql") },
|
||||
new NewtonsoftJsonSerializer(),
|
||||
httpClient
|
||||
);
|
||||
|
||||
System.Version version = await client.GetServerVersion(ct).ConfigureAwait(false);
|
||||
|
||||
// serverMigration property was added in 2.18.5, so only query for it
|
||||
// if the server has been updated past that version
|
||||
System.Version serverMigrationVersion = new(2, 18, 5);
|
||||
|
||||
string queryString;
|
||||
if (version >= serverMigrationVersion)
|
||||
{
|
||||
//language=graphql
|
||||
queryString =
|
||||
"query { activeUser { id name email company avatar streams { totalCount } commits { totalCount } } serverInfo { name company adminContact description version migration { movedFrom movedTo } } }";
|
||||
}
|
||||
else
|
||||
{
|
||||
//language=graphql
|
||||
queryString =
|
||||
"query { activeUser { id name email company avatar streams { totalCount } commits { totalCount } } serverInfo { name company adminContact description version } }";
|
||||
}
|
||||
|
||||
var request = new GraphQLRequest { Query = queryString };
|
||||
|
||||
var response = await client.SendQueryAsync<ActiveUserServerInfoResponse>(request, ct).ConfigureAwait(false);
|
||||
|
||||
if (response.Errors != null)
|
||||
{
|
||||
throw new SpeckleGraphQLException<ActiveUserServerInfoResponse>(
|
||||
$"Query {nameof(GetUserServerInfo)} failed",
|
||||
request,
|
||||
response
|
||||
);
|
||||
}
|
||||
|
||||
ServerInfo serverInfo = response.Data.serverInfo;
|
||||
serverInfo.url = server.ToString().TrimEnd('/');
|
||||
serverInfo.frontend2 = await IsFrontend2Server(server).ConfigureAwait(false);
|
||||
|
||||
return response.Data;
|
||||
//language=graphql
|
||||
queryString =
|
||||
"query { activeUser { id name email company avatar streams { totalCount } commits { totalCount } } serverInfo { name company adminContact description version migration { movedFrom movedTo } } }";
|
||||
}
|
||||
catch (Exception ex) when (!ex.IsFatal())
|
||||
else
|
||||
{
|
||||
throw new SpeckleException($"Failed to get user + server info from {server}", ex);
|
||||
//language=graphql
|
||||
queryString =
|
||||
"query { activeUser { id name email company avatar streams { totalCount } commits { totalCount } } serverInfo { name company adminContact description version } }";
|
||||
}
|
||||
|
||||
var request = new GraphQLRequest { Query = queryString };
|
||||
|
||||
var response = await client.SendQueryAsync<ActiveUserServerInfoResponse>(request, ct).ConfigureAwait(false);
|
||||
|
||||
response.EnsureGraphQLSuccess();
|
||||
|
||||
ServerInfo serverInfo = response.Data.serverInfo;
|
||||
serverInfo.url = server.ToString().TrimEnd('/');
|
||||
serverInfo.frontend2 = await IsFrontend2Server(server).ConfigureAwait(false);
|
||||
|
||||
return response.Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user