mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-22 04:27:19 +01:00
Upgrade to HotChocolate 14 RC. Setup DataLoaders, Filtering, and Sorting.
CBT with new API transformer procedure
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using HotChocolate.Execution;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tgstation.Server.Host.Controllers;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
@@ -12,10 +11,7 @@ using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Authority.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes <typeparamref name="TAuthority"/>s.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAuthority">The <see cref="IAuthority"/> invoked.</typeparam>
|
||||
/// <inheritdoc />
|
||||
sealed class AuthorityInvoker<TAuthority> : IRestAuthorityInvoker<TAuthority>, IGraphQLAuthorityInvoker<TAuthority>
|
||||
where TAuthority : IAuthority
|
||||
{
|
||||
@@ -25,7 +21,7 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
readonly TAuthority authority;
|
||||
|
||||
/// <summary>
|
||||
/// Throws a <see cref="QueryException"/> for errored <paramref name="authorityResponse"/>s.
|
||||
/// Throws a <see cref="ErrorMessageException"/> for errored <paramref name="authorityResponse"/>s.
|
||||
/// </summary>
|
||||
/// <param name="authorityResponse">The potentially errored <paramref name="authorityResponse"/>.</param>
|
||||
static void ThrowGraphQLErrorIfNecessary(AuthorityResponse authorityResponse)
|
||||
@@ -49,16 +45,21 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IActionResult> Invoke(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(authority);
|
||||
return CreateErroredActionResult(controller, authorityResponse) ?? controller.NoContent();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IActionResult> InvokeTransformable<TResult, TApiModel>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
|
||||
where TResult : notnull, IApiTransformable<TApiModel>
|
||||
where TResult : notnull, ILegacyApiTransformable<TApiModel>
|
||||
where TApiModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(authority);
|
||||
var erroredResult = CreateErroredActionResult(controller, authorityResponse);
|
||||
if (erroredResult != null)
|
||||
@@ -72,6 +73,9 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// <inheritdoc />
|
||||
async ValueTask<IActionResult> IRestAuthorityInvoker<TAuthority>.Invoke<TResult, TApiModel>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(authority);
|
||||
var erroredResult = CreateErroredActionResult(controller, authorityResponse);
|
||||
if (erroredResult != null)
|
||||
@@ -84,6 +88,8 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// <inheritdoc />
|
||||
async ValueTask IGraphQLAuthorityInvoker<TAuthority>.Invoke(Func<TAuthority, ValueTask<AuthorityResponse>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(authority);
|
||||
ThrowGraphQLErrorIfNecessary(authorityResponse);
|
||||
}
|
||||
@@ -93,19 +99,42 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
where TResult : TApiModel
|
||||
where TApiModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(authority);
|
||||
ThrowGraphQLErrorIfNecessary(authorityResponse);
|
||||
return authorityResponse.Result!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
async ValueTask<TApiModel> IGraphQLAuthorityInvoker<TAuthority>.InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
|
||||
var authorityResponse = await authorityInvoker(authority);
|
||||
ThrowGraphQLErrorIfNecessary(authorityResponse);
|
||||
return authorityResponse.Result!.ToApi();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IQueryable<TResult> InvokeQueryable<TResult>(Func<TAuthority, IQueryable<TResult>> authorityInvoker)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
return authorityInvoker(authority);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IQueryable<TApiModel> InvokeTransformableQueryable<TResult, TApiModel, TTransformer>(Func<TAuthority, IQueryable<TResult>> authorityInvoker)
|
||||
where TResult : IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new()
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
var expression = new TTransformer().Expression;
|
||||
return authorityInvoker(authority)
|
||||
.Select(expression);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an <see cref="IActionResult"/> for a given <paramref name="authorityResponse"/> if it is erroring.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Authority.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes <typeparamref name="TAuthority"/>s.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAuthority">The <see cref="IAuthority"/> invoked.</typeparam>
|
||||
public interface IAuthorityInvoker<TAuthority>
|
||||
where TAuthority : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The returned <see cref="Type"/>.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="IQueryable{T}"/> <typeparamref name="TResult"/>.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> <typeparamref name="TResult"/> returned.</returns>
|
||||
IQueryable<TResult> InvokeQueryable<TResult>(Func<TAuthority, IQueryable<TResult>> authorityInvoker);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a <typeparamref name="TAuthority"/> method and get the transformed result.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="Type"/> returned by the <typeparamref name="TAuthority"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The returned <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="IQueryable{T}"/> <typeparamref name="TResult"/>.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> <typeparamref name="TResult"/> returned.</returns>
|
||||
IQueryable<TApiModel> InvokeTransformableQueryable<TResult, TApiModel, TTransformer>(Func<TAuthority, IQueryable<TResult>> authorityInvoker)
|
||||
where TResult : IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// Invokes <typeparamref name="TAuthority"/>s from GraphQL endpoints.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAuthority">The <see cref="IAuthority"/> invoked.</typeparam>
|
||||
public interface IGraphQLAuthorityInvoker<TAuthority>
|
||||
public interface IGraphQLAuthorityInvoker<TAuthority> : IAuthorityInvoker<TAuthority>
|
||||
where TAuthority : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
@@ -35,10 +35,12 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult">The <see cref="AuthorityResponse{TResult}.Result"/> <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The resulting <see cref="Type"/> of the return value.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for converting <typeparamref name="TResult"/>s to <typeparamref name="TApiModel"/>s.</typeparam>
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TApiModel"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<TApiModel> InvokeTransformable<TResult, TApiModel>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, IApiTransformable<TApiModel>
|
||||
where TApiModel : notnull;
|
||||
ValueTask<TApiModel> InvokeTransformable<TResult, TApiModel, TTransformer>(Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, IApiTransformable<TResult, TApiModel, TTransformer>
|
||||
where TApiModel : notnull
|
||||
where TTransformer : ITransformer<TResult, TApiModel>, new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// Invokes <typeparamref name="TAuthority"/> methods and generates <see cref="IActionResult"/> responses.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAuthority">The type of <see cref="IAuthority"/>.</typeparam>
|
||||
public interface IRestAuthorityInvoker<TAuthority>
|
||||
public interface IRestAuthorityInvoker<TAuthority> : IAuthorityInvoker<TAuthority>
|
||||
where TAuthority : IAuthority
|
||||
{
|
||||
/// <summary>
|
||||
@@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.Authority.Core
|
||||
/// <param name="authorityInvoker">The <typeparamref name="TAuthority"/> <see cref="Func{T, TResult}"/> returning a <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/>.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> generated for the resulting <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<IActionResult> InvokeTransformable<TResult, TApiModel>(ApiController controller, Func<TAuthority, ValueTask<AuthorityResponse<TResult>>> authorityInvoker)
|
||||
where TResult : notnull, IApiTransformable<TApiModel>
|
||||
where TResult : notnull, ILegacyApiTransformable<TApiModel>
|
||||
where TApiModel : notnull;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// Gets all registered <see cref="User"/>s.
|
||||
/// </summary>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="IQueryable{T}"/> <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> of <see cref="User"/>s.</returns>
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
public ValueTask<AuthorityResponse<IQueryable<User>>> List(bool includeJoins);
|
||||
public IQueryable<User> Queryable(bool includeJoins);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using GreenDonut;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -21,24 +24,55 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
readonly IDatabaseContext databaseContext;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IUsersDataLoader"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IUsersDataLoader dataLoader;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAuthenticationContext"/> for the <see cref="UserAuthority"/>.
|
||||
/// </summary>
|
||||
readonly IAuthenticationContext authenticationContext;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the <see cref="dataLoader"/>.
|
||||
/// </summary>
|
||||
/// <param name="ids">The <see cref="IReadOnlyCollection{T}"/> of <see cref="User"/> <see cref="Api.Models.EntityId.Id"/>s to load.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> to load from.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="Dictionary{TKey, TValue}"/> of the requested <see cref="User"/>s.</returns>
|
||||
[DataLoader]
|
||||
public static async ValueTask<Dictionary<long, User>> GetUsers(
|
||||
IReadOnlyList<long> ids,
|
||||
IDatabaseContext databaseContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(ids);
|
||||
ArgumentNullException.ThrowIfNull(databaseContext);
|
||||
|
||||
return await databaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
.Where(x => ids.Contains(x.Id!.Value))
|
||||
.ToDictionaryAsync(user => user.Id!.Value, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserAuthority"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="databaseContext">The value of <see cref="databaseContext"/>.</param>
|
||||
/// <param name="dataLoader">The value of <see cref="dataLoader"/>.</param>
|
||||
/// <param name="authenticationContext">The value of <see cref="authenticationContext"/>.</param>
|
||||
public UserAuthority(
|
||||
ILogger<UserAuthority> logger,
|
||||
IDatabaseContext databaseContext,
|
||||
IUsersDataLoader dataLoader,
|
||||
IAuthenticationContext authenticationContext)
|
||||
: base(logger)
|
||||
{
|
||||
this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
|
||||
this.dataLoader = dataLoader ?? throw new ArgumentNullException(nameof(dataLoader));
|
||||
this.authenticationContext = authenticationContext ?? throw new ArgumentNullException(nameof(authenticationContext));
|
||||
}
|
||||
|
||||
@@ -49,11 +83,18 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken)
|
||||
{
|
||||
var queryable = ListCore(includeJoins);
|
||||
User? user;
|
||||
if (includeJoins)
|
||||
{
|
||||
var queryable = Queryable(true, true);
|
||||
|
||||
user = await queryable.FirstOrDefaultAsync(
|
||||
dbModel => dbModel.Id == id,
|
||||
cancellationToken);
|
||||
}
|
||||
else
|
||||
user = await dataLoader.LoadAsync(id, cancellationToken);
|
||||
|
||||
var user = await queryable.FirstOrDefaultAsync(
|
||||
dbModel => dbModel.Id == id,
|
||||
cancellationToken);
|
||||
if (user == default)
|
||||
return NotFound<User>();
|
||||
|
||||
@@ -64,26 +105,26 @@ namespace Tgstation.Server.Host.Authority
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<AuthorityResponse<IQueryable<User>>> List(bool includeJoins)
|
||||
{
|
||||
var systemUserCanonicalName = User.CanonicalizeName(User.TgsSystemUserName);
|
||||
return ValueTask.FromResult(
|
||||
new AuthorityResponse<IQueryable<User>>(
|
||||
ListCore(includeJoins)
|
||||
.Where(x => x.CanonicalName != systemUserCanonicalName)));
|
||||
}
|
||||
public IQueryable<User> Queryable(bool includeJoins)
|
||||
=> Queryable(includeJoins, false);
|
||||
|
||||
/// <summary>
|
||||
/// Generates an <see cref="IQueryable{T}"/> for listing <see cref="User"/>s.
|
||||
/// Gets all registered <see cref="User"/>s.
|
||||
/// </summary>
|
||||
/// <param name="includeJoins">If related entities should be loaded.</param>
|
||||
/// <returns>A new <see cref="IQueryable{T}"/> of <see cref="User"/>s.</returns>
|
||||
private IQueryable<User> ListCore(bool includeJoins)
|
||||
/// <param name="allowSystemUser">If the <see cref="User"/> with the <see cref="User.TgsSystemUserName"/> should be included in results.</param>
|
||||
/// <returns>A <see cref="IQueryable{T}"/> of <see cref="User"/>s.</returns>
|
||||
IQueryable<User> Queryable(bool includeJoins, bool allowSystemUser)
|
||||
{
|
||||
var tgsUserCanonicalName = User.CanonicalizeName(User.TgsSystemUserName);
|
||||
var queryable = databaseContext
|
||||
.Users
|
||||
.AsQueryable();
|
||||
|
||||
if (!allowSystemUser)
|
||||
queryable = queryable
|
||||
.Where(user => user.CanonicalName != tgsUserCanonicalName);
|
||||
|
||||
if (includeJoins)
|
||||
queryable = queryable
|
||||
.Include(x => x.CreatedBy)
|
||||
|
||||
@@ -288,7 +288,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
int? pageQuery,
|
||||
int? pageSizeQuery,
|
||||
CancellationToken cancellationToken)
|
||||
where TModel : IApiTransformable<TApiModel>
|
||||
where TModel : ILegacyApiTransformable<TApiModel>
|
||||
=> PaginatedImpl(
|
||||
queryGenerator,
|
||||
resultTransformer,
|
||||
@@ -299,7 +299,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Generates a paginated response.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The <see cref="Type"/> of model being generated. If different from <typeparamref name="TResultModel"/>, must implement <see cref="IApiTransformable{TApiModel}"/> for <typeparamref name="TResultModel"/>.</typeparam>
|
||||
/// <typeparam name="TModel">The <see cref="Type"/> of model being generated. If different from <typeparamref name="TResultModel"/>, must implement <see cref="ILegacyApiTransformable{TApiModel}"/> for <typeparamref name="TResultModel"/>.</typeparam>
|
||||
/// <typeparam name="TResultModel">The <see cref="Type"/> of model being returned.</typeparam>
|
||||
/// <param name="queryGenerator">A <see cref="Func{TResult}"/> resulting in a <see cref="ValueTask{TResult}"/> resulting in the generated <see cref="PaginatableResult{TModel}"/>.</param>
|
||||
/// <param name="resultTransformer">A <see cref="Func{T, TResult}"/> to transform the <typeparamref name="TResultModel"/>s after being queried.</param>
|
||||
@@ -356,7 +356,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
finalResults = (List<TResultModel>)(object)pagedResults; // clearly a safe cast
|
||||
else
|
||||
finalResults = pagedResults
|
||||
.OfType<IApiTransformable<TResultModel>>()
|
||||
.OfType<ILegacyApiTransformable<TResultModel>>()
|
||||
.Select(x => x.ToApi())
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -371,21 +371,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation.</returns>
|
||||
/// <response code="200">Retrieved <see cref="User"/>s successfully.</response>
|
||||
[HttpGet(Routes.List)]
|
||||
[TgsAuthorize(AdministrationRights.ReadUsers)]
|
||||
[TgsRestAuthorize<IUserAuthority>(nameof(IUserAuthority.Queryable))]
|
||||
[ProducesResponseType(typeof(PaginatedResponse<UserResponse>), 200)]
|
||||
public ValueTask<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
|
||||
=> Paginated<User, UserResponse>(
|
||||
() => ValueTask.FromResult(
|
||||
new PaginatableResult<User>(
|
||||
DatabaseContext
|
||||
.Users
|
||||
.AsQueryable()
|
||||
.Where(x => x.CanonicalName != Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
|
||||
.Include(x => x.CreatedBy)
|
||||
.Include(x => x.PermissionSet)
|
||||
.Include(x => x.OAuthConnections)
|
||||
.Include(x => x.Group!)
|
||||
.ThenInclude(x => x.PermissionSet)
|
||||
userAuthority.InvokeQueryable(
|
||||
authority => authority.Queryable(true))
|
||||
.OrderBy(x => x.Id))),
|
||||
null,
|
||||
page,
|
||||
|
||||
@@ -297,10 +297,19 @@ namespace Tgstation.Server.Host.Core
|
||||
.AddGraphQLServer()
|
||||
.AddAuthorization()
|
||||
.AddMutationConventions()
|
||||
.AddGlobalObjectIdentification()
|
||||
.ModifyOptions(options =>
|
||||
{
|
||||
options.EnableDefer = true;
|
||||
})
|
||||
.ModifyPagingOptions(pagingOptions =>
|
||||
{
|
||||
pagingOptions.IncludeTotalCount = true;
|
||||
pagingOptions.RequirePagingBoundaries = false;
|
||||
})
|
||||
.AddFiltering()
|
||||
.AddSorting()
|
||||
.AddHostTypes()
|
||||
.AddErrorFilter<ErrorMessageFilter>()
|
||||
.AddType<LocalGateway>()
|
||||
.AddType<RemoteGateway>()
|
||||
|
||||
@@ -11,7 +11,7 @@ using Tgstation.Server.Host.System;
|
||||
namespace Tgstation.Server.Host.GraphQL.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Management interface for the parent <see cref="Node"/>.
|
||||
/// Management interface for the parent <see cref="SwarmNode"/>.
|
||||
/// </summary>
|
||||
public interface IGateway
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using HotChocolate.Types.Relay;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using HotChocolate.Types.Relay;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
@@ -11,12 +13,20 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// The ID of the <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
[ID]
|
||||
public long Id { get; }
|
||||
public required long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Entity"/> class.
|
||||
/// </summary>
|
||||
protected Entity()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Entity"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The value of <see cref="Id"/>.</param>
|
||||
[SetsRequiredMembers]
|
||||
protected Entity(long id)
|
||||
{
|
||||
Id = id;
|
||||
|
||||
@@ -13,7 +13,7 @@ using Tgstation.Server.Host.System;
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IGateway"/> for the <see cref="Node"/> this query is executing on.
|
||||
/// <see cref="IGateway"/> for the <see cref="SwarmNode"/> this query is executing on.
|
||||
/// </summary>
|
||||
public sealed class LocalGateway : IGateway
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Tgstation.Server.Host.GraphQL.Interfaces;
|
||||
|
||||
@@ -12,12 +13,20 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// <summary>
|
||||
/// The name of the <see cref="NamedEntity"/>.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamedEntity"/> class.
|
||||
/// </summary>
|
||||
protected NamedEntity()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NamedEntity"/> class.
|
||||
/// </summary>
|
||||
/// <param name="copy">The <see cref="IUserName"/> to copy.</param>
|
||||
[SetsRequiredMembers]
|
||||
protected NamedEntity(NamedEntity copy)
|
||||
: base(copy?.Id ?? throw new ArgumentNullException(nameof(copy)))
|
||||
{
|
||||
@@ -29,6 +38,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// </summary>
|
||||
/// <param name="id">The ID for the <see cref="Entity"/>.</param>
|
||||
/// <param name="name">The value of <see cref="Name"/>.</param>
|
||||
[SetsRequiredMembers]
|
||||
protected NamedEntity(long id, string name)
|
||||
: base(id)
|
||||
{
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using HotChocolate;
|
||||
using HotChocolate.Types.Relay;
|
||||
|
||||
using Tgstation.Server.Host.Swarm;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent a server in the TGS server swarm.
|
||||
/// </summary>
|
||||
[Node]
|
||||
public sealed class NodeInformation
|
||||
{
|
||||
public NodeInformation? GetNodeInformation(
|
||||
string identifier,
|
||||
[Service] ISwarmService swarmService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(identifier);
|
||||
ArgumentNullException.ThrowIfNull(swarmService);
|
||||
|
||||
var node = swarmService.GetSwarmServers()
|
||||
?.FirstOrDefault(node => node.Identifier == identifier);
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
return new NodeInformation(node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The swarm server ID.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using HotChocolate.Types.Relay;
|
||||
|
||||
using Tgstation.Server.Api.Rights;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a set of permissions for the server.
|
||||
/// </summary>
|
||||
[Node]
|
||||
public sealed class PermissionSet : Entity
|
||||
{
|
||||
/// <summary>
|
||||
@@ -23,6 +28,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// <param name="id">The <see cref="Entity.Id"/>.</param>
|
||||
/// <param name="administrationRights">The value of <see cref="AdministrationRights"/>.</param>
|
||||
/// <param name="instanceManagerRights">The value of <see cref="InstanceManagerRights"/>.</param>
|
||||
[SetsRequiredMembers]
|
||||
public PermissionSet(long id, AdministrationRights administrationRights, InstanceManagerRights instanceManagerRights)
|
||||
: base(id)
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@ using Tgstation.Server.Host.System;
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IGateway"/> for accessing remote <see cref="Node"/>s.
|
||||
/// <see cref="IGateway"/> for accessing remote <see cref="SwarmNode"/>s.
|
||||
/// </summary>
|
||||
/// <remarks>This is currently unimplemented.</remarks>
|
||||
public sealed class RemoteGateway : IGateway
|
||||
|
||||
@@ -40,12 +40,12 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
public Users Users() => new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connected <see cref="Node"/> server.
|
||||
/// Gets the connected <see cref="SwarmNode"/> server.
|
||||
/// </summary>
|
||||
/// <param name="swarmService">The <see cref="ISwarmService"/> to use.</param>
|
||||
/// <param name="swarmConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the current <see cref="SwarmConfiguration"/>.</param>
|
||||
/// <returns>A new <see cref="Node"/>.</returns>
|
||||
public Node CurrentNode(
|
||||
/// <returns>A new <see cref="SwarmNode"/>.</returns>
|
||||
public SwarmNode CurrentNode(
|
||||
[Service] ISwarmService swarmService,
|
||||
[Service] IOptionsSnapshot<SwarmConfiguration> swarmConfigurationOptions)
|
||||
{
|
||||
@@ -56,19 +56,19 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
if (nodeInfos != null)
|
||||
return nodeInfos.First(x => x.Info!.Identifier == swarmConfigurationOptions.Value.Identifier);
|
||||
|
||||
return new Node(null);
|
||||
return new SwarmNode(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all <see cref="Node"/> servers in the swarm.
|
||||
/// Gets all <see cref="SwarmNode"/> servers in the swarm.
|
||||
/// </summary>
|
||||
/// <param name="swarmService">The <see cref="ISwarmService"/> to use.</param>
|
||||
/// <returns>A <see cref="List{T}"/> of <see cref="Node"/>s if the local server is part of a swarm, <see langword="null"/> otherwise.</returns>
|
||||
public List<Node>? Nodes(
|
||||
/// <returns>A <see cref="List{T}"/> of <see cref="SwarmNode"/>s if the local server is part of a swarm, <see langword="null"/> otherwise.</returns>
|
||||
public List<SwarmNode>? Nodes(
|
||||
[Service] ISwarmService swarmService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(swarmService);
|
||||
return swarmService.GetSwarmServers()?.Select(x => new Node(new NodeInformation(x))).ToList();
|
||||
return swarmService.GetSwarmServers()?.Select(x => new SwarmNode(new NodeInformation(x))).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -12,7 +12,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// <summary>
|
||||
/// Represents a node server in a swarm.
|
||||
/// </summary>
|
||||
public sealed class Node
|
||||
public sealed class SwarmNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the <see cref="NodeInformation"/>.
|
||||
@@ -20,20 +20,20 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
public NodeInformation? Info { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Node"/> class.
|
||||
/// Initializes a new instance of the <see cref="SwarmNode"/> class.
|
||||
/// </summary>
|
||||
/// <param name="info">The value of <see cref="Info"/>.</param>
|
||||
public Node(NodeInformation? info)
|
||||
public SwarmNode(NodeInformation? info)
|
||||
{
|
||||
Info = info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="Node"/>'s <see cref="IGateway"/>.
|
||||
/// Gets the <see cref="SwarmNode"/>'s <see cref="IGateway"/>.
|
||||
/// </summary>
|
||||
/// <param name="swarmConfigurationOptions">The <see cref="IOptionsSnapshot{TOptions}"/> containing the current <see cref="SwarmConfiguration"/>.</param>
|
||||
/// <returns>A new <see cref="IGateway"/>.</returns>
|
||||
/// <remarks>The <see cref="Node"/>'s <see cref="IGateway"/>.</remarks>
|
||||
/// <remarks>The <see cref="SwarmNode"/>'s <see cref="IGateway"/>.</remarks>
|
||||
public IGateway? Gateway([Service] IOptionsSnapshot<SwarmConfiguration> swarmConfigurationOptions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(swarmConfigurationOptions);
|
||||
@@ -4,76 +4,61 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using HotChocolate;
|
||||
using HotChocolate.Types.Relay;
|
||||
|
||||
using Tgstation.Server.Host.Authority;
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
using Tgstation.Server.Host.GraphQL.Interfaces;
|
||||
using Tgstation.Server.Host.Models.Transformers;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// A user registered in the server.
|
||||
/// </summary>
|
||||
[Node]
|
||||
public sealed class User : NamedEntity, IUserName
|
||||
{
|
||||
/// <summary>
|
||||
/// If the <see cref="User"/> is enabled since users cannot be deleted. System users cannot be disabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; }
|
||||
public required bool Enabled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The user's canonical (Uppercase) name.
|
||||
/// </summary>
|
||||
public string CanonicalName { get; }
|
||||
public required string CanonicalName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the <see cref="User"/> was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The SID/UID of the <see cref="User"/> on Windows/POSIX respectively.
|
||||
/// </summary>
|
||||
public string? SystemIdentifier { get; }
|
||||
public required string? SystemIdentifier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Entity.Id"/> of the <see cref="CreatedBy"/> <see cref="User"/>.
|
||||
/// </summary>
|
||||
readonly long? createdById;
|
||||
[GraphQLIgnore]
|
||||
public required long? CreatedById { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Entity.Id"/> of the <see cref="Group"/>.
|
||||
/// </summary>
|
||||
readonly long? groupId;
|
||||
[GraphQLIgnore]
|
||||
public required long? GroupId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="User"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="Entity.Id"/>.</param>
|
||||
/// <param name="name">The <see cref="NamedEntity.Name"/>.</param>
|
||||
/// <param name="canonicalName">The value of <see cref="CanonicalName"/>.</param>
|
||||
/// <param name="systemIdentifier">The value of <see cref="SystemIdentifier"/>.</param>
|
||||
/// <param name="createdAt">The value of <see cref="CreatedAt"/>.</param>
|
||||
/// <param name="createdById">The value of <see cref="createdById"/>.</param>
|
||||
/// <param name="groupId">The value of <see cref="groupId"/>.</param>
|
||||
/// <param name="enabled">The value of <see cref="Enabled"/>.</param>
|
||||
public User(
|
||||
public static ValueTask<User> GetUser(
|
||||
long id,
|
||||
string name,
|
||||
string canonicalName,
|
||||
string? systemIdentifier,
|
||||
DateTimeOffset createdAt,
|
||||
long? createdById,
|
||||
long? groupId,
|
||||
bool enabled)
|
||||
: base(id, name)
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> authorityInvoker,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SystemIdentifier = systemIdentifier;
|
||||
CanonicalName = canonicalName ?? throw new ArgumentNullException(nameof(canonicalName));
|
||||
CreatedAt = createdAt;
|
||||
this.createdById = createdById;
|
||||
Enabled = enabled;
|
||||
this.groupId = groupId;
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
return authorityInvoker.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
authority => authority.GetId(id, false, false, cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -87,10 +72,10 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
if (!createdById.HasValue)
|
||||
if (!CreatedById.HasValue)
|
||||
return null;
|
||||
|
||||
var user = await userAuthority.InvokeTransformable<Models.User, User>(authority => authority.GetId(createdById.Value, false, true, cancellationToken));
|
||||
var user = await userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(authority => authority.GetId(CreatedById.Value, false, true, cancellationToken));
|
||||
if (user.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
|
||||
return new UserName(user);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using HotChocolate.Types;
|
||||
@@ -22,6 +23,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
/// <param name="id">The <see cref="Entity.Id"/>.</param>
|
||||
/// <param name="name">The <see cref="NamedEntity.Name"/>.</param>
|
||||
/// <param name="permissionSetId">The value of <see cref="permissionSetId"/>.</param>
|
||||
[SetsRequiredMembers]
|
||||
public UserGroup(
|
||||
long id,
|
||||
string name,
|
||||
|
||||
@@ -1,16 +1,41 @@
|
||||
using Tgstation.Server.Host.GraphQL.Interfaces;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using HotChocolate;
|
||||
using HotChocolate.Types.Relay;
|
||||
|
||||
using Tgstation.Server.Host.Authority;
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
using Tgstation.Server.Host.GraphQL.Interfaces;
|
||||
using Tgstation.Server.Host.Models.Transformers;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="User"/> with limited fields.
|
||||
/// </summary>
|
||||
[Node]
|
||||
public sealed class UserName : NamedEntity, IUserName
|
||||
{
|
||||
public static async ValueTask<UserName> GetUserName(
|
||||
long id,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> authorityInvoker,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authorityInvoker);
|
||||
var user = await authorityInvoker.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(
|
||||
authority => authority.GetId(id, false, true, cancellationToken));
|
||||
|
||||
return new UserName(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserName"/> class.
|
||||
/// </summary>
|
||||
/// <param name="copy">The <see cref="NamedEntity"/> to copy.</param>
|
||||
[SetsRequiredMembers]
|
||||
public UserName(NamedEntity copy)
|
||||
: base(copy)
|
||||
{
|
||||
|
||||
@@ -4,12 +4,13 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using HotChocolate;
|
||||
using HotChocolate.Data;
|
||||
using HotChocolate.Types;
|
||||
using HotChocolate.Types.Relay;
|
||||
|
||||
using Tgstation.Server.Host.Authority;
|
||||
using Tgstation.Server.Host.Authority.Core;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Models.Transformers;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
#pragma warning disable CA1724 // conflict with GitLabApiClient.Models.Users. They can fuck off
|
||||
@@ -33,7 +34,7 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
return userAuthority.InvokeTransformable<Models.User, User>(authority => authority.Read(cancellationToken));
|
||||
return userAuthority.InvokeTransformable<Models.User, User, UserGraphQLTransformer>(authority => authority.Read(cancellationToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,26 +50,23 @@ namespace Tgstation.Server.Host.GraphQL.Types
|
||||
[ID(nameof(User))] long id,
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
return await userAuthority.InvokeTransformable<Models.User, User>(authority => authority.GetId(id, false, false, cancellationToken));
|
||||
}
|
||||
=> await User.GetUser(id, userAuthority, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all registered <see cref="User"/>s.
|
||||
/// </summary>
|
||||
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> <see cref="IUserAuthority"/>.</param>
|
||||
/// <returns>A list of all registered <see cref="User"/>s.</returns>
|
||||
[UsePaging(IncludeTotalCount = true)]
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.List))]
|
||||
public async ValueTask<IQueryable<User?>> List(
|
||||
[UsePaging]
|
||||
[UseFiltering]
|
||||
[UseSorting]
|
||||
[TgsGraphQLAuthorize<IUserAuthority>(nameof(IUserAuthority.Queryable))]
|
||||
public IQueryable<User>? Queryable(
|
||||
[Service] IGraphQLAuthorityInvoker<IUserAuthority> userAuthority)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(userAuthority);
|
||||
var dtoQueryable = await userAuthority.Invoke<IQueryable<Models.User>, IQueryable<Models.User>>(authority => authority.List(false));
|
||||
return dtoQueryable
|
||||
.Cast<IApiTransformable<User>>()
|
||||
.Select(dto => dto.ToApi());
|
||||
var dtoQueryable = userAuthority.InvokeTransformableQueryable<Models.User, User, UserGraphQLTransformer>(authority => authority.Queryable(false));
|
||||
return dtoQueryable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using Tgstation.Server.Api.Models.Response;
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.Internal.ChatBotSettings" />
|
||||
public sealed class ChatBot : Api.Models.Internal.ChatBotSettings, IApiTransformable<ChatBotResponse>
|
||||
public sealed class ChatBot : Api.Models.Internal.ChatBotSettings, ILegacyApiTransformable<ChatBotResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Default for <see cref="Api.Models.Internal.ChatBotSettings.ChannelLimit"/>.
|
||||
|
||||
@@ -7,7 +7,7 @@ using Tgstation.Server.Api.Models.Response;
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.Internal.CompileJob" />
|
||||
public sealed class CompileJob : Api.Models.Internal.CompileJob, IApiTransformable<CompileJobResponse>
|
||||
public sealed class CompileJob : Api.Models.Internal.CompileJob, ILegacyApiTransformable<CompileJobResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// See <see cref="CompileJobResponse.Job"/>.
|
||||
|
||||
@@ -5,7 +5,7 @@ using Tgstation.Server.Api.Models.Response;
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.Internal.DreamMakerSettings" />
|
||||
public sealed class DreamMakerSettings : Api.Models.Internal.DreamMakerSettings, IApiTransformable<DreamMakerResponse>
|
||||
public sealed class DreamMakerSettings : Api.Models.Internal.DreamMakerSettings, ILegacyApiTransformable<DreamMakerResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// The row Id.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
#pragma warning disable CA1005
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a host-side model that may be transformed into a <typeparamref name="TApiModel"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TModel">The internal model <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TApiModel">The API model <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TTransformer">The <see cref="ITransformer{TModel, TApiModel}"/> <see cref="Type"/>.</typeparam>
|
||||
public interface IApiTransformable<TModel, TApiModel, TTransformer>
|
||||
where TModel : IApiTransformable<TModel, TApiModel, TTransformer>
|
||||
where TTransformer : ITransformer<TModel, TApiModel>, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert the <see cref="IApiTransformable{TModel, TApiModel, TApiTransformer}"/> to it's <typeparamref name="TApiModel"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <typeparamref name="TApiModel"/> based on the <see cref="IApiTransformable{TModel, TApiModel, TApiTransformer}"/>.</returns>
|
||||
TApiModel ToApi()
|
||||
=> new TTransformer()
|
||||
.CompiledExpression((TModel)this);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -4,12 +4,12 @@
|
||||
/// Represents a host-side model that may be transformed into a <typeparamref name="TApiModel"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TApiModel">The API form of the model.</typeparam>
|
||||
public interface IApiTransformable<TApiModel>
|
||||
public interface ILegacyApiTransformable<out TApiModel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert the <see cref="IApiTransformable{TApiModel}"/> to it's <typeparamref name="TApiModel"/>.
|
||||
/// Convert the <see cref="ILegacyApiTransformable{TApiModel}"/> to it's <typeparamref name="TApiModel"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <typeparamref name="TApiModel"/> based on the <see cref="IApiTransformable{TApiModel}"/>.</returns>
|
||||
/// <returns>A new <typeparamref name="TApiModel"/> based on the <see cref="ILegacyApiTransformable{TApiModel}"/>.</returns>
|
||||
TApiModel ToApi();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains a transformation <see cref="Expression"/> for converting <typeparamref name="TInput"/>s to <typeparamref name="TOutput"/>s.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInput">The input <see cref="Type"/>.</typeparam>
|
||||
/// <typeparam name="TOutput">The output <see cref="Type"/>.</typeparam>
|
||||
public interface ITransformer<TInput, TOutput>
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Expression{TDelegate}"/> form of the transformation.
|
||||
/// </summary>
|
||||
Expression<Func<TInput, TOutput>> Expression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The compiled <see cref="Expression"/>.
|
||||
/// </summary>
|
||||
Func<TInput, TOutput> CompiledExpression { get; }
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <summary>
|
||||
/// Represents an <see cref="Api.Models.Instance"/> in the database.
|
||||
/// </summary>
|
||||
public sealed class Instance : Api.Models.Instance, IApiTransformable<InstanceResponse>
|
||||
public sealed class Instance : Api.Models.Instance, ILegacyApiTransformable<InstanceResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// Default for <see cref="Api.Models.Instance.ChatBotLimit"/>.
|
||||
|
||||
@@ -5,7 +5,7 @@ using Tgstation.Server.Api.Models.Response;
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.Internal.InstancePermissionSet" />
|
||||
public sealed class InstancePermissionSet : Api.Models.Internal.InstancePermissionSet, IApiTransformable<InstancePermissionSetResponse>
|
||||
public sealed class InstancePermissionSet : Api.Models.Internal.InstancePermissionSet, ILegacyApiTransformable<InstancePermissionSetResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// The row Id.
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.Internal.Job" />
|
||||
#pragma warning disable CA1724 // naming conflict with gitlab package
|
||||
public sealed class Job : Api.Models.Internal.Job, IApiTransformable<JobResponse>
|
||||
public sealed class Job : Api.Models.Internal.Job, ILegacyApiTransformable<JobResponse>
|
||||
#pragma warning restore CA1724
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.OAuthConnection" />
|
||||
public sealed class OAuthConnection : Api.Models.OAuthConnection, IApiTransformable<Api.Models.OAuthConnection>
|
||||
public sealed class OAuthConnection : Api.Models.OAuthConnection, ILegacyApiTransformable<Api.Models.OAuthConnection>
|
||||
{
|
||||
/// <summary>
|
||||
/// The row Id.
|
||||
|
||||
@@ -5,7 +5,7 @@ using Tgstation.Server.Api.Models.Response;
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.RepositorySettings" />
|
||||
public sealed class RepositorySettings : Api.Models.RepositorySettings, IApiTransformable<RepositoryResponse>
|
||||
public sealed class RepositorySettings : Api.Models.RepositorySettings, ILegacyApiTransformable<RepositoryResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// The row Id.
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Linq;
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.Internal.RevisionInformation" />
|
||||
public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation, IApiTransformable<Api.Models.RevisionInformation>
|
||||
public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation, ILegacyApiTransformable<Api.Models.RevisionInformation>
|
||||
{
|
||||
/// <summary>
|
||||
/// The row Id.
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.Internal.TestMergeApiBase" />
|
||||
public sealed class TestMerge : Api.Models.Internal.TestMergeApiBase, IApiTransformable<Api.Models.TestMerge>
|
||||
public sealed class TestMerge : Api.Models.Internal.TestMergeApiBase, ILegacyApiTransformable<Api.Models.TestMerge>
|
||||
{
|
||||
/// <summary>
|
||||
/// See <see cref="Api.Models.TestMerge.MergedBy"/>.
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Tgstation.Server.Host.Models.Transformers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
abstract class TransformerBase<TInput, TOutput> : ITransformer<TInput, TOutput>
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="static"/> cache for <see cref="CompiledExpression"/>.
|
||||
/// </summary>
|
||||
static Func<TInput, TOutput>? compiledExpression;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Expression<Func<TInput, TOutput>> Expression { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Func<TInput, TOutput> CompiledExpression { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TransformerBase{TInput, TOutput}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="expression">The value of <see cref="Expression"/>.</param>
|
||||
protected TransformerBase(
|
||||
Expression<Func<TInput, TOutput>> expression)
|
||||
{
|
||||
compiledExpression ??= expression.Compile();
|
||||
Expression = expression;
|
||||
CompiledExpression = compiledExpression;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Tgstation.Server.Host.Models.Transformers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ITransformer{TInput, TOutput}"/> for <see cref="GraphQL.Types.User"/>s.
|
||||
/// </summary>
|
||||
sealed class UserGraphQLTransformer : TransformerBase<User, GraphQL.Types.User>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserGraphQLTransformer"/> class.
|
||||
/// </summary>
|
||||
public UserGraphQLTransformer()
|
||||
: base(model => new GraphQL.Types.User
|
||||
{
|
||||
CreatedAt = model.CreatedAt!.Value,
|
||||
CanonicalName = model.CanonicalName!,
|
||||
CreatedById = model.CreatedById,
|
||||
Enabled = model.Enabled!.Value,
|
||||
GroupId = model.GroupId,
|
||||
Id = model.Id!.Value,
|
||||
Name = model.Name!,
|
||||
SystemIdentifier = model.SystemIdentifier,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,14 @@ using System.Linq;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Models.Transformers;
|
||||
|
||||
namespace Tgstation.Server.Host.Models
|
||||
{
|
||||
/// <inheritdoc cref="Api.Models.Internal.UserModelBase" />
|
||||
public sealed class User : Api.Models.Internal.UserModelBase, IApiTransformable<UserResponse>, IApiTransformable<GraphQL.Types.User>
|
||||
public sealed class User : Api.Models.Internal.UserModelBase,
|
||||
ILegacyApiTransformable<UserResponse>,
|
||||
IApiTransformable<User, GraphQL.Types.User, UserGraphQLTransformer>
|
||||
{
|
||||
/// <summary>
|
||||
/// Username used when creating jobs automatically.
|
||||
@@ -83,18 +86,6 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <inheritdoc />
|
||||
public UserResponse ToApi() => CreateUserResponse(true);
|
||||
|
||||
/// <inheritdoc />
|
||||
GraphQL.Types.User IApiTransformable<GraphQL.Types.User>.ToApi()
|
||||
=> new(
|
||||
Id!.Value,
|
||||
Name!,
|
||||
CanonicalName!,
|
||||
SystemIdentifier,
|
||||
CreatedAt!.Value,
|
||||
CreatedById,
|
||||
GroupId,
|
||||
Enabled!.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a <see cref="UserResponse"/> from <see langword="this"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <summary>
|
||||
/// Represents a group of <see cref="User"/>s.
|
||||
/// </summary>
|
||||
public sealed class UserGroup : NamedEntity, IApiTransformable<UserGroupResponse>
|
||||
public sealed class UserGroup : NamedEntity, ILegacyApiTransformable<UserGroupResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Models.PermissionSet"/> the <see cref="UserGroup"/> has.
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using GreenDonut;
|
||||
|
||||
[assembly: InternalsVisibleTo("Tgstation.Server.Host.Tests")]
|
||||
[assembly: InternalsVisibleTo("Tgstation.Server.Host.Tests.Signals")]
|
||||
[assembly: InternalsVisibleTo("Tgstation.Server.Tests")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
|
||||
[assembly: DataLoaderDefaults(AccessModifier = DataLoaderAccessModifier.Internal)]
|
||||
|
||||
@@ -100,11 +100,15 @@
|
||||
<!-- Usage: GitLab interop -->
|
||||
<PackageReference Include="GitLabApiClient" Version="1.8.0" />
|
||||
<!-- Usage: GraphQL API Engine -->
|
||||
<PackageReference Include="HotChocolate.AspNetCore" Version="13.9.12" />
|
||||
<PackageReference Include="HotChocolate.AspNetCore" Version="14.0.0-rc.1" />
|
||||
<!-- Usage: GraphQL Authorization Plugin -->
|
||||
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="13.9.12" />
|
||||
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="14.0.0-rc.1" />
|
||||
<!-- Usage: GraphQL IDatabaseContext support -->
|
||||
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="14.0.0-rc.1" />
|
||||
<!-- Usage: DataLoader source generation -->
|
||||
<PackageReference Include="HotChocolate.Types.Analyzers" Version="14.0.0-rc.1" />
|
||||
<!-- Usage: GraphQL additional scalar type definitions -->
|
||||
<PackageReference Include="HotChocolate.Types.Scalars" Version="13.9.12" />
|
||||
<PackageReference Include="HotChocolate.Types.Scalars" Version="14.0.0-rc.1" />
|
||||
<!-- Usage: git interop -->
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.30.0" />
|
||||
<!-- Usage: Support ""legacy"" Newotonsoft.Json in HTTP pipeline. The rest of our codebase uses Newtonsoft. -->
|
||||
|
||||
Reference in New Issue
Block a user