Upgrade to HotChocolate 14 RC. Setup DataLoaders, Filtering, and Sorting.

CBT with new API transformer procedure
This commit is contained in:
Jordan Dominion
2024-09-11 22:50:36 -04:00
parent ede12a619b
commit 673bfcb31b
41 changed files with 415 additions and 144 deletions
@@ -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();
}
}
}
@@ -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);
+19 -34
View File
@@ -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;
}
}
}
+1 -1
View File
@@ -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);
}
}
@@ -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; }
}
}
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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,
})
{
}
}
}
+4 -13
View File
@@ -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. -->