GraphQL API Initial Implementation

- Build API via annotations using HotChocolate.
  - Export to `artifacts` on build.
  - Repurpose REST API types where possible, but many have to be reimplemented.
  - Transfer controller and bridge controller will definitely have to stay forever.
- Added `Tgstation.Server.Client.GraphQL` built from schema using StrawberryShake.
  - Added equivalent server information query.
- Setup semver and u32 (de)serialization.
- Begin transitioning to multi-client type live tests.
- Add GraphQL route `/api/graphql`
- Rename ServerClient likes to RestServerClient equivalent.
- Major API/Client library version bumps.
This commit is contained in:
Jordan Dominion
2024-09-08 20:51:04 -04:00
parent b4b8119648
commit d89461d19d
64 changed files with 1338 additions and 174 deletions
@@ -5,7 +5,7 @@ using System.Net.Http;
namespace Tgstation.Server.Client
{
/// <summary>
/// Exceptions thrown by <see cref="IServerClient"/>s.
/// Exceptions thrown by <see cref="IRestServerClient"/>s.
/// </summary>
public abstract class ClientException : Exception
{
+2 -21
View File
@@ -1,5 +1,4 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -8,14 +7,13 @@ using Microsoft.Extensions.Logging;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
namespace Tgstation.Server.Client
{
/// <summary>
/// Web interface for the API.
/// </summary>
interface IApiClient : IAsyncDisposable
interface IApiClient : ITransferClient, IAsyncDisposable
{
/// <summary>
/// The <see cref="ApiHeaders"/> the <see cref="IApiClient"/> uses.
@@ -39,7 +37,7 @@ namespace Tgstation.Server.Client
void AddRequestLogger(IRequestLogger requestLogger);
/// <summary>
/// Subscribe to all job updates available to the <see cref="IServerClient"/>.
/// Subscribe to all job updates available to the <see cref="IRestServerClient"/>.
/// </summary>
/// <typeparam name="THubImplementation">The <see cref="Type"/> of the hub being implemented.</typeparam>
/// <param name="hubImplementation">The <typeparamref name="THubImplementation"/> to use for proxying the methods of the hub connection.</param>
@@ -239,22 +237,5 @@ namespace Tgstation.Server.Client
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask<TResult> Delete<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken)
where TBody : class;
/// <summary>
/// Downloads a file <see cref="Stream"/> for a given <paramref name="ticket"/>.
/// </summary>
/// <param name="ticket">The <see cref="FileTicketResponse"/> to download.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the downloaded <see cref="Stream"/>.</returns>
ValueTask<Stream> Download(FileTicketResponse ticket, CancellationToken cancellationToken);
/// <summary>
/// Uploads a given <paramref name="uploadStream"/> for a given <paramref name="ticket"/>.
/// </summary>
/// <param name="ticket">The <see cref="FileTicketResponse"/> to download.</param>
/// <param name="uploadStream">The <see cref="Stream"/> to upload. <see langword="null"/> represents an empty file.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask Upload(FileTicketResponse ticket, Stream? uploadStream, CancellationToken cancellationToken);
}
}
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Client
/// <summary>
/// Main client for communicating with a server.
/// </summary>
public interface IServerClient : IAsyncDisposable
public interface IRestServerClient : IAsyncDisposable
{
/// <summary>
/// The connected server's root <see cref="Uri"/>.
@@ -51,14 +51,20 @@ namespace Tgstation.Server.Client
IUserGroupsClient Groups { get; }
/// <summary>
/// The <see cref="ServerInformationResponse"/> of the <see cref="IServerClient"/>.
/// Access the <see cref="ITransferClient"/>.
/// </summary>
/// <remarks>Most client methods handle transfers in their invocations. There is rarely any reason to use the <see cref="ITransferClient"/> directly.</remarks>
ITransferClient Transfer { get; }
/// <summary>
/// The <see cref="ServerInformationResponse"/> of the <see cref="IRestServerClient"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ServerInformationResponse"/> of the target server.</returns>
ValueTask<ServerInformationResponse> ServerInformation(CancellationToken cancellationToken);
/// <summary>
/// Subscribe to all job updates available to the <see cref="IServerClient"/>.
/// Subscribe to all job updates available to the <see cref="IRestServerClient"/>.
/// </summary>
/// <param name="jobsReceiver">The <see cref="IJobsHub"/> to use to subscribe to updates.</param>
/// <param name="retryPolicy">The optional <see cref="IRetryPolicy"/> to use for the backing connection. The default retry policy waits for 1, 2, 4, 8, and 16 seconds, then 30s repeatedly.</param>
@@ -9,9 +9,9 @@ using Tgstation.Server.Api.Models.Response;
namespace Tgstation.Server.Client
{
/// <summary>
/// Factory for creating <see cref="IServerClient"/>s.
/// Factory for creating <see cref="IRestServerClient"/>s.
/// </summary>
public interface IServerClientFactory
public interface IRestServerClientFactory
{
/// <summary>
/// Gets the <see cref="ServerInformationResponse"/> for a given <paramref name="host"/>.
@@ -28,17 +28,17 @@ namespace Tgstation.Server.Client
CancellationToken cancellationToken = default);
/// <summary>
/// Create a <see cref="IServerClient"/> using a password login.
/// Create a <see cref="IRestServerClient"/> using a password login.
/// </summary>
/// <param name="host">The URL to access TGS.</param>
/// <param name="username">The username to for the <see cref="IServerClient"/>.</param>
/// <param name="password">The password for the <see cref="IServerClient"/>.</param>
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IServerClient"/>.</param>
/// <param name="username">The username to for the <see cref="IRestServerClient"/>.</param>
/// <param name="password">The password for the <see cref="IRestServerClient"/>.</param>
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IRestServerClient"/>.</param>
/// <param name="timeout">Optional <see cref="TimeSpan"/> representing timeout for the connection.</param>
/// <param name="attemptLoginRefresh">Attempt to refresh the received <see cref="TokenResponse"/> when it expires or becomes invalid. <paramref name="username"/> and <paramref name="password"/> will be stored in memory if this is <see langword="true"/>.</param>
/// <param name="cancellationToken">Optional <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IServerClient"/>.</returns>
ValueTask<IServerClient> CreateFromLogin(
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IRestServerClient"/>.</returns>
ValueTask<IRestServerClient> CreateFromLogin(
Uri host,
string username,
string password,
@@ -48,16 +48,16 @@ namespace Tgstation.Server.Client
CancellationToken cancellationToken = default);
/// <summary>
/// Create a <see cref="IServerClient"/> using am OAuth login.
/// Create a <see cref="IRestServerClient"/> using an OAuth login.
/// </summary>
/// <param name="host">The URL to access TGS.</param>
/// <param name="oAuthCode">The OAuth code used to complete the flow.</param>
/// <param name="oAuthProvider">The <see cref="OAuthProvider"/>.</param>
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IServerClient"/>.</param>
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IRestServerClient"/>.</param>
/// <param name="timeout">Optional <see cref="TimeSpan"/> representing timeout for the connection.</param>
/// <param name="cancellationToken">Optional <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IServerClient"/>.</returns>
ValueTask<IServerClient> CreateFromOAuth(
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IRestServerClient"/>.</returns>
ValueTask<IRestServerClient> CreateFromOAuth(
Uri host,
string oAuthCode,
OAuthProvider oAuthProvider,
@@ -66,12 +66,12 @@ namespace Tgstation.Server.Client
CancellationToken cancellationToken = default);
/// <summary>
/// Create a <see cref="IServerClient"/>.
/// Create a <see cref="IRestServerClient"/>.
/// </summary>
/// <param name="host">The URL to access TGS.</param>
/// <param name="token">The <see cref="TokenResponse"/> to access the API with.</param>
/// <returns>A new <see cref="IServerClient"/>.</returns>
IServerClient CreateFromToken(
/// <returns>A new <see cref="IRestServerClient"/>.</returns>
IRestServerClient CreateFromToken(
Uri host,
TokenResponse token);
}
@@ -0,0 +1,31 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models.Response;
namespace Tgstation.Server.Client
{
/// <summary>
/// For transferring data <see cref="Stream"/>s.
/// </summary>
public interface ITransferClient
{
/// <summary>
/// Downloads a file <see cref="Stream"/> for a given <paramref name="ticket"/>.
/// </summary>
/// <param name="ticket">The <see cref="FileTicketResponse"/> to download.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the downloaded <see cref="Stream"/>.</returns>
ValueTask<Stream> Download(FileTicketResponse ticket, CancellationToken cancellationToken);
/// <summary>
/// Uploads a given <paramref name="uploadStream"/> for a given <paramref name="ticket"/>.
/// </summary>
/// <param name="ticket">The <see cref="FileTicketResponse"/> to download.</param>
/// <param name="uploadStream">The <see cref="Stream"/> to upload. <see langword="null"/> represents an empty file.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask Upload(FileTicketResponse ticket, Stream? uploadStream, CancellationToken cancellationToken);
}
}
@@ -12,7 +12,7 @@ using Tgstation.Server.Api.Models.Response;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class ServerClient : IServerClient
sealed class RestServerClient : IRestServerClient
{
/// <inheritdoc />
public Uri Url => apiClient.Url;
@@ -43,16 +43,19 @@ namespace Tgstation.Server.Client
/// <inheritdoc />
public IUserGroupsClient Groups { get; }
/// <inheritdoc />
public ITransferClient Transfer => apiClient;
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="ServerClient"/>.
/// The <see cref="IApiClient"/> for the <see cref="RestServerClient"/>.
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// Initializes a new instance of the <see cref="ServerClient"/> class.
/// Initializes a new instance of the <see cref="RestServerClient"/> class.
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/>.</param>
public ServerClient(IApiClient apiClient)
public RestServerClient(IApiClient apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
@@ -12,37 +12,37 @@ using Tgstation.Server.Api.Models.Response;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
public sealed class ServerClientFactory : IServerClientFactory
public sealed class RestServerClientFactory : IRestServerClientFactory
{
/// <summary>
/// The <see cref="IApiClientFactory"/> for the <see cref="ServerClientFactory"/>.
/// The <see cref="IApiClientFactory"/> for the <see cref="RestServerClientFactory"/>.
/// </summary>
internal static IApiClientFactory ApiClientFactory { get; set; }
/// <summary>
/// The <see cref="ProductHeaderValue"/> for the <see cref="ServerClientFactory"/>.
/// The <see cref="ProductHeaderValue"/> for the <see cref="RestServerClientFactory"/>.
/// </summary>
readonly ProductHeaderValue productHeaderValue;
/// <summary>
/// Initializes static members of the <see cref="ServerClientFactory"/> class.
/// Initializes static members of the <see cref="RestServerClientFactory"/> class.
/// </summary>
static ServerClientFactory()
static RestServerClientFactory()
{
ApiClientFactory = new ApiClientFactory();
}
/// <summary>
/// Initializes a new instance of the <see cref="ServerClientFactory"/> class.
/// Initializes a new instance of the <see cref="RestServerClientFactory"/> class.
/// </summary>
/// <param name="productHeaderValue">The value of <see cref="productHeaderValue"/>.</param>
public ServerClientFactory(ProductHeaderValue productHeaderValue)
public RestServerClientFactory(ProductHeaderValue productHeaderValue)
{
this.productHeaderValue = productHeaderValue ?? throw new ArgumentNullException(nameof(productHeaderValue));
}
/// <inheritdoc />
public ValueTask<IServerClient> CreateFromLogin(
public ValueTask<IRestServerClient> CreateFromLogin(
Uri host,
string username,
string password,
@@ -69,7 +69,7 @@ namespace Tgstation.Server.Client
}
/// <inheritdoc />
public ValueTask<IServerClient> CreateFromOAuth(
public ValueTask<IRestServerClient> CreateFromOAuth(
Uri host,
string oAuthCode,
OAuthProvider oAuthProvider,
@@ -93,7 +93,7 @@ namespace Tgstation.Server.Client
}
/// <inheritdoc />
public IServerClient CreateFromToken(Uri host, TokenResponse token)
public IRestServerClient CreateFromToken(Uri host, TokenResponse token)
{
if (host == null)
throw new ArgumentNullException(nameof(host));
@@ -102,7 +102,7 @@ namespace Tgstation.Server.Client
if (token.Bearer == null)
throw new InvalidOperationException("token.Bearer should not be null!");
var serverClient = new ServerClient(
var serverClient = new RestServerClient(
ApiClientFactory.CreateApiClient(
host,
new ApiHeaders(
@@ -142,16 +142,16 @@ namespace Tgstation.Server.Client
}
/// <summary>
/// Creates a <see cref="IServerClient"/> from a login operation.
/// Creates a <see cref="IRestServerClient"/> from a login operation.
/// </summary>
/// <param name="host">The URL to access TGS.</param>
/// <param name="loginHeaders">The <see cref="ApiHeaders"/> to use for the login operation.</param>
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IServerClient"/>.</param>
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IRestServerClient"/>.</param>
/// <param name="timeout">Optional <see cref="TimeSpan"/> representing timeout for the connection.</param>
/// <param name="attemptLoginRefresh">If <paramref name="loginHeaders"/> may be used to re-login in the future.</param>
/// <param name="cancellationToken">Optional <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IServerClient"/>.</returns>
async ValueTask<IServerClient> CreateWithNewToken(
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IRestServerClient"/>.</returns>
async ValueTask<IRestServerClient> CreateWithNewToken(
Uri host,
ApiHeaders loginHeaders,
IEnumerable<IRequestLogger>? requestLoggers,
@@ -173,7 +173,7 @@ namespace Tgstation.Server.Client
}
var apiHeaders = new ApiHeaders(productHeaderValue, token);
var client = new ServerClient(
var client = new RestServerClient(
ApiClientFactory.CreateApiClient(
host,
apiHeaders,