diff --git a/src/Tgstation.Server.Host/Authority/Core/AuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/AuthorityInvoker{TAuthority}.cs index 1756c62cf8..db3f06c803 100644 --- a/src/Tgstation.Server.Host/Authority/Core/AuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/AuthorityInvoker{TAuthority}.cs @@ -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 { - /// - /// Invokes s. - /// - /// The invoked. + /// sealed class AuthorityInvoker : IRestAuthorityInvoker, IGraphQLAuthorityInvoker where TAuthority : IAuthority { @@ -25,7 +21,7 @@ namespace Tgstation.Server.Host.Authority.Core readonly TAuthority authority; /// - /// Throws a for errored s. + /// Throws a for errored s. /// /// The potentially errored . static void ThrowGraphQLErrorIfNecessary(AuthorityResponse authorityResponse) @@ -49,16 +45,21 @@ namespace Tgstation.Server.Host.Authority.Core /// public async ValueTask Invoke(ApiController controller, Func> authorityInvoker) { + ArgumentNullException.ThrowIfNull(controller); + ArgumentNullException.ThrowIfNull(authorityInvoker); + var authorityResponse = await authorityInvoker(authority); return CreateErroredActionResult(controller, authorityResponse) ?? controller.NoContent(); } /// public async ValueTask InvokeTransformable(ApiController controller, Func>> authorityInvoker) - - where TResult : notnull, IApiTransformable + where TResult : notnull, ILegacyApiTransformable 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 /// async ValueTask IRestAuthorityInvoker.Invoke(ApiController controller, Func>> 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 /// async ValueTask IGraphQLAuthorityInvoker.Invoke(Func> 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!; } /// - async ValueTask IGraphQLAuthorityInvoker.InvokeTransformable(Func>> authorityInvoker) + async ValueTask IGraphQLAuthorityInvoker.InvokeTransformable(Func>> authorityInvoker) { + ArgumentNullException.ThrowIfNull(authorityInvoker); + var authorityResponse = await authorityInvoker(authority); ThrowGraphQLErrorIfNecessary(authorityResponse); return authorityResponse.Result!.ToApi(); } + /// + public IQueryable InvokeQueryable(Func> authorityInvoker) + { + ArgumentNullException.ThrowIfNull(authorityInvoker); + return authorityInvoker(authority); + } + + /// + public IQueryable InvokeTransformableQueryable(Func> authorityInvoker) + where TResult : IApiTransformable + where TApiModel : notnull + where TTransformer : ITransformer, new() + { + ArgumentNullException.ThrowIfNull(authorityInvoker); + var expression = new TTransformer().Expression; + return authorityInvoker(authority) + .Select(expression); + } + /// /// Create an for a given if it is erroring. /// diff --git a/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs new file mode 100644 index 0000000000..9b49bd9dda --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/Core/IAuthorityInvoker{TAuthority}.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; + +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Authority.Core +{ + /// + /// Invokes s. + /// + /// The invoked. + public interface IAuthorityInvoker + where TAuthority : IAuthority + { + /// + /// Invoke a method and get the result. + /// + /// The returned . + /// The returning a . + /// A returned. + IQueryable InvokeQueryable(Func> authorityInvoker); + + /// + /// Invoke a method and get the transformed result. + /// + /// The returned by the . + /// The returned . + /// The for converting s to s. + /// The returning a . + /// A returned. + IQueryable InvokeTransformableQueryable(Func> authorityInvoker) + where TResult : IApiTransformable + where TApiModel : notnull + where TTransformer : ITransformer, new(); + } +} diff --git a/src/Tgstation.Server.Host/Authority/Core/IGraphQLAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/IGraphQLAuthorityInvoker{TAuthority}.cs index 7ce01fa637..1c5031edc3 100644 --- a/src/Tgstation.Server.Host/Authority/Core/IGraphQLAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/IGraphQLAuthorityInvoker{TAuthority}.cs @@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Authority.Core /// Invokes s from GraphQL endpoints. /// /// The invoked. - public interface IGraphQLAuthorityInvoker + public interface IGraphQLAuthorityInvoker : IAuthorityInvoker where TAuthority : IAuthority { /// @@ -35,10 +35,12 @@ namespace Tgstation.Server.Host.Authority.Core /// /// The . /// The resulting of the return value. + /// The for converting s to s. /// The returning a resulting in the . /// A resulting in the generated for the resulting . - ValueTask InvokeTransformable(Func>> authorityInvoker) - where TResult : notnull, IApiTransformable - where TApiModel : notnull; + ValueTask InvokeTransformable(Func>> authorityInvoker) + where TResult : notnull, IApiTransformable + where TApiModel : notnull + where TTransformer : ITransformer, new(); } } diff --git a/src/Tgstation.Server.Host/Authority/Core/IRestAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/Core/IRestAuthorityInvoker{TAuthority}.cs index 55e61f34cf..564de05c2a 100644 --- a/src/Tgstation.Server.Host/Authority/Core/IRestAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/IRestAuthorityInvoker{TAuthority}.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Authority.Core /// Invokes methods and generates responses. /// /// The type of . - public interface IRestAuthorityInvoker + public interface IRestAuthorityInvoker : IAuthorityInvoker where TAuthority : IAuthority { /// @@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.Authority.Core /// The returning a resulting in the . /// A resulting in the generated for the resulting . ValueTask InvokeTransformable(ApiController controller, Func>> authorityInvoker) - where TResult : notnull, IApiTransformable + where TResult : notnull, ILegacyApiTransformable where TApiModel : notnull; } } diff --git a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs index 71f807371a..dfc9cdda9d 100644 --- a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs @@ -38,8 +38,8 @@ namespace Tgstation.Server.Host.Authority /// Gets all registered s. /// /// If related entities should be loaded. - /// A resulting in a . + /// A of s. [TgsAuthorize(AdministrationRights.ReadUsers)] - public ValueTask>> List(bool includeJoins); + public IQueryable Queryable(bool includeJoins); } } diff --git a/src/Tgstation.Server.Host/Authority/UserAuthority.cs b/src/Tgstation.Server.Host/Authority/UserAuthority.cs index fe15593a1b..ddb20d4108 100644 --- a/src/Tgstation.Server.Host/Authority/UserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/UserAuthority.cs @@ -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 /// readonly IDatabaseContext databaseContext; + /// + /// The for the . + /// + readonly IUsersDataLoader dataLoader; + /// /// The for the . /// readonly IAuthenticationContext authenticationContext; + /// + /// Implements the . + /// + /// The of s to load. + /// The to load from. + /// The for the operation. + /// A resulting in a of the requested s. + [DataLoader] + public static async ValueTask> GetUsers( + IReadOnlyList 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); + } + /// /// Initializes a new instance of the class. /// /// The to use. /// The value of . + /// The value of . /// The value of . public UserAuthority( ILogger 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 /// public async ValueTask> 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(); @@ -64,26 +105,26 @@ namespace Tgstation.Server.Host.Authority } /// - public ValueTask>> List(bool includeJoins) - { - var systemUserCanonicalName = User.CanonicalizeName(User.TgsSystemUserName); - return ValueTask.FromResult( - new AuthorityResponse>( - ListCore(includeJoins) - .Where(x => x.CanonicalName != systemUserCanonicalName))); - } + public IQueryable Queryable(bool includeJoins) + => Queryable(includeJoins, false); /// - /// Generates an for listing s. + /// Gets all registered s. /// /// If related entities should be loaded. - /// A new of s. - private IQueryable ListCore(bool includeJoins) + /// If the with the should be included in results. + /// A of s. + IQueryable 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) diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 709dfa7087..439b3e3c07 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -288,7 +288,7 @@ namespace Tgstation.Server.Host.Controllers int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken) - where TModel : IApiTransformable + where TModel : ILegacyApiTransformable => PaginatedImpl( queryGenerator, resultTransformer, @@ -299,7 +299,7 @@ namespace Tgstation.Server.Host.Controllers /// /// Generates a paginated response. /// - /// The of model being generated. If different from , must implement for . + /// The of model being generated. If different from , must implement for . /// The of model being returned. /// A resulting in a resulting in the generated . /// A to transform the s after being queried. @@ -356,7 +356,7 @@ namespace Tgstation.Server.Host.Controllers finalResults = (List)(object)pagedResults; // clearly a safe cast else finalResults = pagedResults - .OfType>() + .OfType>() .Select(x => x.ToApi()) .ToList(); diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index e59df1f889..0965ad26b5 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -371,21 +371,14 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the operation. /// Retrieved s successfully. [HttpGet(Routes.List)] - [TgsAuthorize(AdministrationRights.ReadUsers)] + [TgsRestAuthorize(nameof(IUserAuthority.Queryable))] [ProducesResponseType(typeof(PaginatedResponse), 200)] public ValueTask List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) => Paginated( () => ValueTask.FromResult( new PaginatableResult( - 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, diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 41d20aba8e..1b07bbb9c5 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -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() .AddType() .AddType() diff --git a/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs b/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs index 89acbc10cc..e8846abbea 100644 --- a/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs +++ b/src/Tgstation.Server.Host/GraphQL/Interfaces/IGateway.cs @@ -11,7 +11,7 @@ using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.GraphQL.Interfaces { /// - /// Management interface for the parent . + /// Management interface for the parent . /// public interface IGateway { diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Entity.cs b/src/Tgstation.Server.Host/GraphQL/Types/Entity.cs index ab51d61a6f..4f45c22306 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/Entity.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/Entity.cs @@ -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 . /// [ID] - public long Id { get; } + public required long Id { get; init; } + + /// + /// Initializes a new instance of the class. + /// + protected Entity() + { + } /// /// Initializes a new instance of the class. /// /// The value of . + [SetsRequiredMembers] protected Entity(long id) { Id = id; diff --git a/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs b/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs index 1774bc6596..107df473e1 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/LocalGateway.cs @@ -13,7 +13,7 @@ using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.GraphQL.Types { /// - /// for the this query is executing on. + /// for the this query is executing on. /// public sealed class LocalGateway : IGateway { diff --git a/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs b/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs index a52677b834..d0a36221cc 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/NamedEntity.cs @@ -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 /// /// The name of the . /// - public string Name { get; } + public required string Name { get; init; } + + /// + /// Initializes a new instance of the class. + /// + protected NamedEntity() + { + } /// /// Initializes a new instance of the class. /// /// The to copy. + [SetsRequiredMembers] protected NamedEntity(NamedEntity copy) : base(copy?.Id ?? throw new ArgumentNullException(nameof(copy))) { @@ -29,6 +38,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// The ID for the . /// The value of . + [SetsRequiredMembers] protected NamedEntity(long id, string name) : base(id) { diff --git a/src/Tgstation.Server.Host/GraphQL/Types/NodeInformation.cs b/src/Tgstation.Server.Host/GraphQL/Types/NodeInformation.cs index 0661bf92c9..546d77d9f2 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/NodeInformation.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/NodeInformation.cs @@ -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 { /// /// Represent a server in the TGS server swarm. /// + [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); + } + /// /// The swarm server ID. /// diff --git a/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs b/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs index 6d7b0d6b5f..9f0734f316 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/PermissionSet.cs @@ -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 { /// /// Represents a set of permissions for the server. /// + [Node] public sealed class PermissionSet : Entity { /// @@ -23,6 +28,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// The . /// The value of . /// The value of . + [SetsRequiredMembers] public PermissionSet(long id, AdministrationRights administrationRights, InstanceManagerRights instanceManagerRights) : base(id) { diff --git a/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs b/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs index 0a00849057..5c3305e56d 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/RemoteGateway.cs @@ -14,7 +14,7 @@ using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.GraphQL.Types { /// - /// for accessing remote s. + /// for accessing remote s. /// /// This is currently unimplemented. public sealed class RemoteGateway : IGateway diff --git a/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs b/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs index e43d9c5b2e..1b874c13b1 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/ServerSwarm.cs @@ -40,12 +40,12 @@ namespace Tgstation.Server.Host.GraphQL.Types public Users Users() => new(); /// - /// Gets the connected server. + /// Gets the connected server. /// /// The to use. /// The containing the current . - /// A new . - public Node CurrentNode( + /// A new . + public SwarmNode CurrentNode( [Service] ISwarmService swarmService, [Service] IOptionsSnapshot 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); } /// - /// Gets all servers in the swarm. + /// Gets all servers in the swarm. /// /// The to use. - /// A of s if the local server is part of a swarm, otherwise. - public List? Nodes( + /// A of s if the local server is part of a swarm, otherwise. + public List? 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(); } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Node.cs b/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs similarity index 79% rename from src/Tgstation.Server.Host/GraphQL/Types/Node.cs rename to src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs index f42183950d..8558256247 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/Node.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/SwarmNode.cs @@ -12,7 +12,7 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// Represents a node server in a swarm. /// - public sealed class Node + public sealed class SwarmNode { /// /// Gets the . @@ -20,20 +20,20 @@ namespace Tgstation.Server.Host.GraphQL.Types public NodeInformation? Info { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The value of . - public Node(NodeInformation? info) + public SwarmNode(NodeInformation? info) { Info = info; } /// - /// Gets the 's . + /// Gets the 's . /// /// The containing the current . /// A new . - /// The 's . + /// The 's . public IGateway? Gateway([Service] IOptionsSnapshot swarmConfigurationOptions) { ArgumentNullException.ThrowIfNull(swarmConfigurationOptions); diff --git a/src/Tgstation.Server.Host/GraphQL/Types/User.cs b/src/Tgstation.Server.Host/GraphQL/Types/User.cs index 4ab07675df..efacb5525e 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/User.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/User.cs @@ -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 { /// /// A user registered in the server. /// + [Node] public sealed class User : NamedEntity, IUserName { /// /// If the is enabled since users cannot be deleted. System users cannot be disabled. /// - public bool Enabled { get; } + public required bool Enabled { get; init; } /// /// The user's canonical (Uppercase) name. /// - public string CanonicalName { get; } + public required string CanonicalName { get; init; } /// /// When the was created. /// - public DateTimeOffset CreatedAt { get; } + public required DateTimeOffset CreatedAt { get; init; } /// /// The SID/UID of the on Windows/POSIX respectively. /// - public string? SystemIdentifier { get; } + public required string? SystemIdentifier { get; init; } /// /// The of the . /// - readonly long? createdById; + [GraphQLIgnore] + public required long? CreatedById { get; init; } /// /// The of the . /// - readonly long? groupId; + [GraphQLIgnore] + public required long? GroupId { get; init; } - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The . - /// The value of . - /// The value of . - /// The value of . - /// The value of . - /// The value of . - /// The value of . - public User( + public static ValueTask GetUser( long id, - string name, - string canonicalName, - string? systemIdentifier, - DateTimeOffset createdAt, - long? createdById, - long? groupId, - bool enabled) - : base(id, name) + [Service] IGraphQLAuthorityInvoker 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( + authority => authority.GetId(id, false, false, cancellationToken)); } /// @@ -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(authority => authority.GetId(createdById.Value, false, true, cancellationToken)); + var user = await userAuthority.InvokeTransformable(authority => authority.GetId(CreatedById.Value, false, true, cancellationToken)); if (user.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) return new UserName(user); diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs index 82b6242221..7a647e0b88 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs @@ -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 /// The . /// The . /// The value of . + [SetsRequiredMembers] public UserGroup( long id, string name, diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs index f16379debc..3379741f91 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs @@ -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 { /// /// A with limited fields. /// + [Node] public sealed class UserName : NamedEntity, IUserName { + public static async ValueTask GetUserName( + long id, + [Service] IGraphQLAuthorityInvoker authorityInvoker, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(authorityInvoker); + var user = await authorityInvoker.InvokeTransformable( + authority => authority.GetId(id, false, true, cancellationToken)); + + return new UserName(user); + } + /// /// Initializes a new instance of the class. /// /// The to copy. + [SetsRequiredMembers] public UserName(NamedEntity copy) : base(copy) { diff --git a/src/Tgstation.Server.Host/GraphQL/Types/Users.cs b/src/Tgstation.Server.Host/GraphQL/Types/Users.cs index 754771e917..fcba0f00ea 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/Users.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/Users.cs @@ -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(authority => authority.Read(cancellationToken)); + return userAuthority.InvokeTransformable(authority => authority.Read(cancellationToken)); } /// @@ -49,26 +50,23 @@ namespace Tgstation.Server.Host.GraphQL.Types [ID(nameof(User))] long id, [Service] IGraphQLAuthorityInvoker userAuthority, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(userAuthority); - return await userAuthority.InvokeTransformable(authority => authority.GetId(id, false, false, cancellationToken)); - } + => await User.GetUser(id, userAuthority, cancellationToken); /// /// Lists all registered s. /// /// The . /// A list of all registered s. - [UsePaging(IncludeTotalCount = true)] - [TgsGraphQLAuthorize(nameof(IUserAuthority.List))] - public async ValueTask> List( + [UsePaging] + [UseFiltering] + [UseSorting] + [TgsGraphQLAuthorize(nameof(IUserAuthority.Queryable))] + public IQueryable? Queryable( [Service] IGraphQLAuthorityInvoker userAuthority) { ArgumentNullException.ThrowIfNull(userAuthority); - var dtoQueryable = await userAuthority.Invoke, IQueryable>(authority => authority.List(false)); - return dtoQueryable - .Cast>() - .Select(dto => dto.ToApi()); + var dtoQueryable = userAuthority.InvokeTransformableQueryable(authority => authority.Queryable(false)); + return dtoQueryable; } } } diff --git a/src/Tgstation.Server.Host/Models/ChatBot.cs b/src/Tgstation.Server.Host/Models/ChatBot.cs index caf1f0c645..908581eff5 100644 --- a/src/Tgstation.Server.Host/Models/ChatBot.cs +++ b/src/Tgstation.Server.Host/Models/ChatBot.cs @@ -8,7 +8,7 @@ using Tgstation.Server.Api.Models.Response; namespace Tgstation.Server.Host.Models { /// - public sealed class ChatBot : Api.Models.Internal.ChatBotSettings, IApiTransformable + public sealed class ChatBot : Api.Models.Internal.ChatBotSettings, ILegacyApiTransformable { /// /// Default for . diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs index 1ce860be6e..d316f18bd3 100644 --- a/src/Tgstation.Server.Host/Models/CompileJob.cs +++ b/src/Tgstation.Server.Host/Models/CompileJob.cs @@ -7,7 +7,7 @@ using Tgstation.Server.Api.Models.Response; namespace Tgstation.Server.Host.Models { /// - public sealed class CompileJob : Api.Models.Internal.CompileJob, IApiTransformable + public sealed class CompileJob : Api.Models.Internal.CompileJob, ILegacyApiTransformable { /// /// See . diff --git a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs index 5e55dc9683..3904c8ad5d 100644 --- a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs +++ b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs @@ -5,7 +5,7 @@ using Tgstation.Server.Api.Models.Response; namespace Tgstation.Server.Host.Models { /// - public sealed class DreamMakerSettings : Api.Models.Internal.DreamMakerSettings, IApiTransformable + public sealed class DreamMakerSettings : Api.Models.Internal.DreamMakerSettings, ILegacyApiTransformable { /// /// The row Id. diff --git a/src/Tgstation.Server.Host/Models/IApiTransformable{TModel,TApiModel,TTransformer}.cs b/src/Tgstation.Server.Host/Models/IApiTransformable{TModel,TApiModel,TTransformer}.cs new file mode 100644 index 0000000000..df45b7d7e8 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/IApiTransformable{TModel,TApiModel,TTransformer}.cs @@ -0,0 +1,25 @@ +using System; + +#pragma warning disable CA1005 + +namespace Tgstation.Server.Host.Models +{ + /// + /// Represents a host-side model that may be transformed into a . + /// + /// The internal model . + /// The API model . + /// The . + public interface IApiTransformable + where TModel : IApiTransformable + where TTransformer : ITransformer, new() + { + /// + /// Convert the to it's . + /// + /// A new based on the . + TApiModel ToApi() + => new TTransformer() + .CompiledExpression((TModel)this); + } +} diff --git a/src/Tgstation.Server.Host/Models/IApiTransformable.cs b/src/Tgstation.Server.Host/Models/ILegacyApiTransformable{TApiModel}.cs similarity index 61% rename from src/Tgstation.Server.Host/Models/IApiTransformable.cs rename to src/Tgstation.Server.Host/Models/ILegacyApiTransformable{TApiModel}.cs index e2b807b62e..b8064202d8 100644 --- a/src/Tgstation.Server.Host/Models/IApiTransformable.cs +++ b/src/Tgstation.Server.Host/Models/ILegacyApiTransformable{TApiModel}.cs @@ -4,12 +4,12 @@ /// Represents a host-side model that may be transformed into a . /// /// The API form of the model. - public interface IApiTransformable + public interface ILegacyApiTransformable { /// - /// Convert the to it's . + /// Convert the to it's . /// - /// A new based on the . + /// A new based on the . TApiModel ToApi(); } } diff --git a/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs b/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs new file mode 100644 index 0000000000..9da15bcfb7 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq.Expressions; + +namespace Tgstation.Server.Host.Models +{ + /// + /// Contains a transformation for converting s to s. + /// + /// The input . + /// The output . + public interface ITransformer + { + /// + /// form of the transformation. + /// + Expression> Expression { get; } + + /// + /// The compiled . + /// + Func CompiledExpression { get; } + } +} diff --git a/src/Tgstation.Server.Host/Models/Instance.cs b/src/Tgstation.Server.Host/Models/Instance.cs index 35434d81e1..ded8b8fd55 100644 --- a/src/Tgstation.Server.Host/Models/Instance.cs +++ b/src/Tgstation.Server.Host/Models/Instance.cs @@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Models /// /// Represents an in the database. /// - public sealed class Instance : Api.Models.Instance, IApiTransformable + public sealed class Instance : Api.Models.Instance, ILegacyApiTransformable { /// /// Default for . diff --git a/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs b/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs index 8db1f17fba..be4e5357d1 100644 --- a/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs +++ b/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs @@ -5,7 +5,7 @@ using Tgstation.Server.Api.Models.Response; namespace Tgstation.Server.Host.Models { /// - public sealed class InstancePermissionSet : Api.Models.Internal.InstancePermissionSet, IApiTransformable + public sealed class InstancePermissionSet : Api.Models.Internal.InstancePermissionSet, ILegacyApiTransformable { /// /// The row Id. diff --git a/src/Tgstation.Server.Host/Models/Job.cs b/src/Tgstation.Server.Host/Models/Job.cs index 85327de2e2..521e671be8 100644 --- a/src/Tgstation.Server.Host/Models/Job.cs +++ b/src/Tgstation.Server.Host/Models/Job.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Models { /// #pragma warning disable CA1724 // naming conflict with gitlab package - public sealed class Job : Api.Models.Internal.Job, IApiTransformable + public sealed class Job : Api.Models.Internal.Job, ILegacyApiTransformable #pragma warning restore CA1724 { /// diff --git a/src/Tgstation.Server.Host/Models/OAuthConnection.cs b/src/Tgstation.Server.Host/Models/OAuthConnection.cs index d622b358c8..64b1702d8c 100644 --- a/src/Tgstation.Server.Host/Models/OAuthConnection.cs +++ b/src/Tgstation.Server.Host/Models/OAuthConnection.cs @@ -1,7 +1,7 @@ namespace Tgstation.Server.Host.Models { /// - public sealed class OAuthConnection : Api.Models.OAuthConnection, IApiTransformable + public sealed class OAuthConnection : Api.Models.OAuthConnection, ILegacyApiTransformable { /// /// The row Id. diff --git a/src/Tgstation.Server.Host/Models/RepositorySettings.cs b/src/Tgstation.Server.Host/Models/RepositorySettings.cs index d804162718..9f36e2f123 100644 --- a/src/Tgstation.Server.Host/Models/RepositorySettings.cs +++ b/src/Tgstation.Server.Host/Models/RepositorySettings.cs @@ -5,7 +5,7 @@ using Tgstation.Server.Api.Models.Response; namespace Tgstation.Server.Host.Models { /// - public sealed class RepositorySettings : Api.Models.RepositorySettings, IApiTransformable + public sealed class RepositorySettings : Api.Models.RepositorySettings, ILegacyApiTransformable { /// /// The row Id. diff --git a/src/Tgstation.Server.Host/Models/RevisionInformation.cs b/src/Tgstation.Server.Host/Models/RevisionInformation.cs index eb99816ab6..d683e9c8b7 100644 --- a/src/Tgstation.Server.Host/Models/RevisionInformation.cs +++ b/src/Tgstation.Server.Host/Models/RevisionInformation.cs @@ -6,7 +6,7 @@ using System.Linq; namespace Tgstation.Server.Host.Models { /// - public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation, IApiTransformable + public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation, ILegacyApiTransformable { /// /// The row Id. diff --git a/src/Tgstation.Server.Host/Models/TestMerge.cs b/src/Tgstation.Server.Host/Models/TestMerge.cs index 3641640378..1aaf31594a 100644 --- a/src/Tgstation.Server.Host/Models/TestMerge.cs +++ b/src/Tgstation.Server.Host/Models/TestMerge.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models { /// - public sealed class TestMerge : Api.Models.Internal.TestMergeApiBase, IApiTransformable + public sealed class TestMerge : Api.Models.Internal.TestMergeApiBase, ILegacyApiTransformable { /// /// See . diff --git a/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TModel,TApiModel}.cs b/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TModel,TApiModel}.cs new file mode 100644 index 0000000000..e3d16441cc --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TModel,TApiModel}.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq.Expressions; + +namespace Tgstation.Server.Host.Models.Transformers +{ + /// + abstract class TransformerBase : ITransformer + { + /// + /// cache for . + /// + static Func? compiledExpression; + + /// + public Expression> Expression { get; } + + /// + public Func CompiledExpression { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + protected TransformerBase( + Expression> expression) + { + compiledExpression ??= expression.Compile(); + Expression = expression; + CompiledExpression = compiledExpression; + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs b/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs new file mode 100644 index 0000000000..9c1184ecce --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs @@ -0,0 +1,26 @@ +namespace Tgstation.Server.Host.Models.Transformers +{ + /// + /// for s. + /// + sealed class UserGraphQLTransformer : TransformerBase + { + /// + /// Initializes a new instance of the class. + /// + 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, + }) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index d45d579d60..46c5530da1 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -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 { /// - public sealed class User : Api.Models.Internal.UserModelBase, IApiTransformable, IApiTransformable + public sealed class User : Api.Models.Internal.UserModelBase, + ILegacyApiTransformable, + IApiTransformable { /// /// Username used when creating jobs automatically. @@ -83,18 +86,6 @@ namespace Tgstation.Server.Host.Models /// public UserResponse ToApi() => CreateUserResponse(true); - /// - GraphQL.Types.User IApiTransformable.ToApi() - => new( - Id!.Value, - Name!, - CanonicalName!, - SystemIdentifier, - CreatedAt!.Value, - CreatedById, - GroupId, - Enabled!.Value); - /// /// Generate a from . /// diff --git a/src/Tgstation.Server.Host/Models/UserGroup.cs b/src/Tgstation.Server.Host/Models/UserGroup.cs index 4451c1a073..c8fa3efa79 100644 --- a/src/Tgstation.Server.Host/Models/UserGroup.cs +++ b/src/Tgstation.Server.Host/Models/UserGroup.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.Models /// /// Represents a group of s. /// - public sealed class UserGroup : NamedEntity, IApiTransformable + public sealed class UserGroup : NamedEntity, ILegacyApiTransformable { /// /// The the has. diff --git a/src/Tgstation.Server.Host/Properties/AssemblyInfo.cs b/src/Tgstation.Server.Host/Properties/AssemblyInfo.cs index 47d191b80a..23e85d3ef1 100644 --- a/src/Tgstation.Server.Host/Properties/AssemblyInfo.cs +++ b/src/Tgstation.Server.Host/Properties/AssemblyInfo.cs @@ -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)] diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 3192e8faa9..72b62df1f3 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -100,11 +100,15 @@ - + - + + + + + - +