diff --git a/build/analyzers.ruleset b/build/analyzers.ruleset index 15ba71dc89..214eba9ecc 100644 --- a/build/analyzers.ruleset +++ b/build/analyzers.ruleset @@ -1,10 +1,10 @@ - + - + diff --git a/src/Tgstation.Server.Host/Authority/Core/AuthorityResponse{TResult}.cs b/src/Tgstation.Server.Host/Authority/Core/AuthorityResponse{TResult}.cs index 42ec4ffe12..361337c7a8 100644 --- a/src/Tgstation.Server.Host/Authority/Core/AuthorityResponse{TResult}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/AuthorityResponse{TResult}.cs @@ -13,6 +13,11 @@ namespace Tgstation.Server.Host.Authority.Core /// The of success response. public sealed class AuthorityResponse : AuthorityResponse { + /// + /// An expression to convert a into an . The resulting MUST NOT be used. + /// + public static Expression>> MappingExpression { get; } + /// [MemberNotNullWhen(true, nameof(IsNoContent))] public override bool Success => base.Success; @@ -34,11 +39,16 @@ namespace Tgstation.Server.Host.Authority.Core /// public HttpSuccessResponse? SuccessResponse { get; } - public static Expression>> MappingExpression() - => result => new AuthorityResponse + /// + /// Initializes static members of the class. + /// + static AuthorityResponse() + { + MappingExpression = result => new AuthorityResponse { Result = result, }; + } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Authority/Core/Projectable{TQueried,TResult}.cs b/src/Tgstation.Server.Host/Authority/Core/Projectable{TQueried,TResult}.cs index c7858b498d..4ca6c0dd2a 100644 --- a/src/Tgstation.Server.Host/Authority/Core/Projectable{TQueried,TResult}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/Projectable{TQueried,TResult}.cs @@ -14,8 +14,8 @@ namespace Tgstation.Server.Host.Authority.Core /// /// A projectable based on an underlying . /// - /// The DB model type queried. - /// The transformed result. + /// The model queried. + /// The transformed result . public sealed class Projectable where TQueried : EntityId where TResult : notnull @@ -26,14 +26,14 @@ namespace Tgstation.Server.Host.Authority.Core readonly IQueryable query; /// - /// The selector for the data required by the . + /// The selector for the data required by the . /// - readonly Expression, Projected>> selector; + readonly Expression, ProjectedPair>> selector; /// - /// Mapper for transforming the into an . Called with the output of as . + /// Mapper for transforming the into an . Called with the output of as . /// - readonly Func?, AuthorityResponse> resultMapper; + readonly Func?, AuthorityResponse> resultMapper; /// /// for the operation. @@ -43,10 +43,10 @@ namespace Tgstation.Server.Host.Authority.Core /// /// Combine a set of s into the resulting s. /// - /// The projection used to each of the . + /// The projection used to each of the . /// The s to combine. /// A of the resulting s keyed by their . - public static async ValueTask>> Combine(Func, IQueryable>> projection, params Projectable[] inputs) + public static async ValueTask>> Combine(Func, IQueryable>> projection, params Projectable[] inputs) { ArgumentNullException.ThrowIfNull(projection); ArgumentNullException.ThrowIfNull(inputs); @@ -76,13 +76,20 @@ namespace Tgstation.Server.Host.Authority.Core result => firstProjectable.resultMapper(result.Projected)); } + /// + /// Create a . + /// + /// The underlying . + /// A mapper for taking a new (Who's will be ) with its set amd converting it into an . + /// The for the operations on . + /// A new . public static Projectable Create( IQueryable query, - Func?, AuthorityResponse> resultMapper, + Func?, AuthorityResponse> resultMapper, CancellationToken cancellationToken) => Create( query, - projected => new Projected + projected => new ProjectedPair { Queried = null, Result = projected.Result, @@ -90,22 +97,31 @@ namespace Tgstation.Server.Host.Authority.Core resultMapper, cancellationToken); + /// + /// Create a . + /// + /// A selected out of to be used in . + /// The underlying . + /// Selects a value to be set as the value for later use in . + /// A mapper for taking a new (Who's will be the result of on the original .) with its set amd converting it into an . + /// The for the operations on . + /// A new . public static Projectable Create( IQueryable query, - Expression, Projected>> selector, - Func?, AuthorityResponse> resultMapper, + Expression, ProjectedPair>> selector, + Func?, AuthorityResponse> resultMapper, CancellationToken cancellationToken) { - Expression, Projected>> makeProjectedGeneric = projected => new Projected + Expression, ProjectedPair>> makeProjectedGeneric = projected => new ProjectedPair { Queried = projected.Queried, Result = projected.Result, }; - var parameter = Expression.Parameter(typeof(Projected), "innerProjectedParam"); + var parameter = Expression.Parameter(typeof(ProjectedPair), "innerProjectedParam"); return new( query, - Expression.Lambda, Projected>>( + Expression.Lambda, ProjectedPair>>( Expression.Invoke( makeProjectedGeneric, Expression.Invoke( @@ -114,7 +130,7 @@ namespace Tgstation.Server.Host.Authority.Core parameter), projected => resultMapper( projected != null - ? new Projected + ? new ProjectedPair { Queried = (TSelection)projected.Queried!, Result = projected.Result, @@ -124,43 +140,15 @@ namespace Tgstation.Server.Host.Authority.Core } /// - /// Initializes a new instance of the class. + /// Create an to transform a into a . /// - /// The value of . - /// The value of . - /// The value of . - /// The value of . - private Projectable( - IQueryable query, - Expression, Projected>> selector, - Func?, AuthorityResponse> resultMapper, - CancellationToken cancellationToken) + /// The selector used for to get business data for the . + /// A new transforming a into a . + private static Expression, CombinedProjection>> CombinedProjectionExpression(Expression, ProjectedPair>> selector) { - this.query = query ?? throw new ArgumentNullException(nameof(query)); - this.selector = selector ?? throw new ArgumentNullException(nameof(selector)); - this.resultMapper = resultMapper ?? throw new ArgumentNullException(nameof(resultMapper)); - this.cancellationToken = cancellationToken; - } + Expression, long>> idSelector = projected => projected.Queried.Id!.Value; - /// - /// Resolve the . - /// - /// The mapping from to . - /// The resolved . - public async ValueTask> Resolve(Func, IQueryable>> projection) - { - ArgumentNullException.ThrowIfNull(projection); - var selection = await projection(query) - .Select(selector) - .FirstOrDefaultAsync(cancellationToken); - return resultMapper(selection); - } - - private static Expression, CombinedProjection>> CombinedProjectionExpression(Expression, Projected>> selector) - { - Expression, long>> idSelector = projected => projected.Queried.Id!.Value; - - var parameter = Expression.Parameter(typeof(Projected), "projectedParamForCombined"); + var parameter = Expression.Parameter(typeof(ProjectedPair), "projectedParamForCombined"); var selection = Expression.Invoke(selector, parameter); var id = Expression.Invoke(idSelector, parameter); @@ -175,16 +163,58 @@ namespace Tgstation.Server.Host.Authority.Core ourType.GetProperty(nameof(CombinedProjection.Projected))!, selection)); - var finalExpr = Expression.Lambda, CombinedProjection>>(memberInitExpr, parameter); + var finalExpr = Expression.Lambda, CombinedProjection>>(memberInitExpr, parameter); return finalExpr; } - public class CombinedProjection + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + private Projectable( + IQueryable query, + Expression, ProjectedPair>> selector, + Func?, AuthorityResponse> resultMapper, + CancellationToken cancellationToken) { - public long Id { get; init; } + this.query = query ?? throw new ArgumentNullException(nameof(query)); + this.selector = selector ?? throw new ArgumentNullException(nameof(selector)); + this.resultMapper = resultMapper ?? throw new ArgumentNullException(nameof(resultMapper)); + this.cancellationToken = cancellationToken; + } - public Projected Projected { get; init; } + /// + /// Resolve the . + /// + /// The mapping from to . + /// The resolved . + public async ValueTask> Resolve(Func, IQueryable>> projection) + { + ArgumentNullException.ThrowIfNull(projection); + var selection = await projection(query) + .Select(selector) + .FirstOrDefaultAsync(cancellationToken); + return resultMapper(selection); + } + + /// + /// DTO for selecting the from alongside the selection . + /// + private class CombinedProjection + { + /// + /// The of . + /// + public required long Id { get; init; } + + /// + /// The selection/ . + /// + public required ProjectedPair Projected { get; init; } } } } diff --git a/src/Tgstation.Server.Host/Authority/Core/ProjectedPair{TQueried,TResult}.cs b/src/Tgstation.Server.Host/Authority/Core/ProjectedPair{TQueried,TResult}.cs new file mode 100644 index 0000000000..9f80fe2f8a --- /dev/null +++ b/src/Tgstation.Server.Host/Authority/Core/ProjectedPair{TQueried,TResult}.cs @@ -0,0 +1,20 @@ +namespace Tgstation.Server.Host.Authority.Core +{ + /// + /// DTO for moving database projected s through the system. + /// + /// The originally queried . + /// The output DTO . + public sealed class ProjectedPair + { + /// + /// The originally queried . + /// + public required TQueried Queried { get; init; } + + /// + /// The output DTO . + /// + public required TResult Result { get; init; } + } +} diff --git a/src/Tgstation.Server.Host/Authority/Core/Projected{TQueried,TResult}.cs b/src/Tgstation.Server.Host/Authority/Core/Projected{TQueried,TResult}.cs deleted file mode 100644 index 81fc1d6f48..0000000000 --- a/src/Tgstation.Server.Host/Authority/Core/Projected{TQueried,TResult}.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Tgstation.Server.Host.Authority.Core -{ - public class Projected - { - public TQueried Queried { get; init; } - - public TResult Result { get; init; } - } -} diff --git a/src/Tgstation.Server.Host/Authority/Core/RequirementsGated{TResult}.cs b/src/Tgstation.Server.Host/Authority/Core/RequirementsGated{TResult}.cs index 042c577f4e..7522e1a653 100644 --- a/src/Tgstation.Server.Host/Authority/Core/RequirementsGated{TResult}.cs +++ b/src/Tgstation.Server.Host/Authority/Core/RequirementsGated{TResult}.cs @@ -40,9 +40,7 @@ namespace Tgstation.Server.Host.Authority.Core /// /// The to convert. /// A new based on . -#pragma warning disable CA1000 // Do not declare static members on generic types public static RequirementsGated FromResult(TResult result) -#pragma warning restore CA1000 // Do not declare static members on generic types => new( () => (IAuthorizationRequirement?)null, () => ValueTask.FromResult(result)); diff --git a/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs b/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs index 7d9ef0063b..12d044844a 100644 --- a/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs +++ b/src/Tgstation.Server.Host/Authority/IGraphQLAuthorityInvoker{TAuthority}.cs @@ -91,6 +91,16 @@ namespace Tgstation.Server.Host.Authority where TApiModel : notnull where TTransformer : ITransformer, new(); + /// + /// Execute a batch loaded call on a given . + /// + /// The queried result of the . + /// The returned . + /// The for converting s to s. + /// The returning a from to . + /// The list of s to batch load. + /// The active mapped to an of the . MUST have been wrapped with . This will be unwrapped so only a of will be used. + /// A resulting in a of loaded s to s for the loaded s. ValueTask>> ExecuteDataLoader( Func>> authorityInvoker, IReadOnlyList ids, diff --git a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs index b0ad6333f2..26c7ee0846 100644 --- a/src/Tgstation.Server.Host/Authority/IUserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/IUserAuthority.cs @@ -30,6 +30,14 @@ namespace Tgstation.Server.Host.Authority /// A . RequirementsGated> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken); + /// + /// Gets the with a given . + /// + /// The result type after projection. + /// The of the . + /// If the may be returned. + /// The for the operation. + /// A for . RequirementsGated> GetId(long id, bool allowSystemUser, CancellationToken cancellationToken) where TResult : class; diff --git a/src/Tgstation.Server.Host/Authority/UserAuthority.cs b/src/Tgstation.Server.Host/Authority/UserAuthority.cs index 506bd165c6..416df9ff7c 100644 --- a/src/Tgstation.Server.Host/Authority/UserAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/UserAuthority.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -663,7 +662,7 @@ namespace Tgstation.Server.Host.Authority Queryable(true, allowSystemUser) .Where(user => user.Id == id) .TagWith("User by ID"), - projected => new Projected + projected => new ProjectedPair { Queried = projected.Queried.CanonicalName!, Result = projected.Result, diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 5b05b04321..1d3c9cb662 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -11,8 +11,6 @@ using Cyberboss.AspNetCore.AsyncInitializer; using Elastic.CommonSchema.Serilog; using HotChocolate.AspNetCore; -using HotChocolate.Subscriptions; -using HotChocolate.Types; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; @@ -47,7 +45,6 @@ using Serilog.Sinks.Elasticsearch; using Tgstation.Server.Api; using Tgstation.Server.Api.Hubs; using Tgstation.Server.Api.Models; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Authority; using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Components; @@ -65,11 +62,7 @@ using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Controllers.Results; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; -using Tgstation.Server.Host.GraphQL; -using Tgstation.Server.Host.GraphQL.Metadata; -using Tgstation.Server.Host.GraphQL.Scalars; using Tgstation.Server.Host.GraphQL.Subscriptions; -using Tgstation.Server.Host.GraphQL.Types; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Properties; @@ -336,7 +329,7 @@ namespace Tgstation.Server.Host.Core // configure graphql services - .AddScoped() + .AddScoped() .AddGraphQLServer() .ConfigureGraphQLServer(); diff --git a/src/Tgstation.Server.Host/Extensions/DataLoaderExtensions.cs b/src/Tgstation.Server.Host/Extensions/DataLoaderExtensions.cs index c484c62f06..dd6a076f27 100644 --- a/src/Tgstation.Server.Host/Extensions/DataLoaderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/DataLoaderExtensions.cs @@ -9,15 +9,26 @@ using Tgstation.Server.Host.Authority.Core; namespace Tgstation.Server.Host.Extensions { + /// + /// Extension methods for . + /// static class DataLoaderExtensions { - public static async ValueTask LoadAuthorityResponse( - this IDataLoader> dataLoader, + /// + /// Convert a request for a single into a data-loader invocation for the matching . + /// + /// The of the underlying DTO being loaded. + /// The for s. + /// The active , if any. + /// The of to load. + /// The for the operation. + /// A resulting in the loaded if it exists, otherwise. + public static async ValueTask LoadAuthorityResponse( + this IDataLoader> dataLoader, QueryContext? queryContext, - TID id, + long id, CancellationToken cancellationToken) where TResult : class - where TID : notnull { ArgumentNullException.ThrowIfNull(dataLoader); diff --git a/src/Tgstation.Server.Host/Extensions/ExpressionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ExpressionExtensions.cs index 2ea1efaecc..f30b89a78d 100644 --- a/src/Tgstation.Server.Host/Extensions/ExpressionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ExpressionExtensions.cs @@ -10,23 +10,30 @@ namespace Tgstation.Server.Host.Extensions /// static class ExpressionExtensions { - public static Expression>> Projected(this Expression> translationExpression) + /// + /// Create an to transform a given into a and output them as a . + /// + /// The input . + /// The output . + /// An to transform a given into a . + /// An to transform a given into a and output them as a . + public static Expression>> Projected(this Expression> translationExpression) { var parameter = Expression.Parameter(typeof(TQueried), "queried"); var body = Expression.Invoke(translationExpression, parameter); - var ourType = typeof(Projected); + var ourType = typeof(ProjectedPair); var expr = Expression.MemberInit( Expression.New(ourType), Expression.Bind( - ourType.GetProperty(nameof(Authority.Core.Projected.Queried))!, + ourType.GetProperty(nameof(ProjectedPair.Queried))!, parameter), Expression.Bind( - ourType.GetProperty(nameof(Authority.Core.Projected.Result))!, + ourType.GetProperty(nameof(ProjectedPair.Result))!, body)); - return Expression.Lambda>>(expr, parameter); + return Expression.Lambda>>(expr, parameter); } } } diff --git a/src/Tgstation.Server.Host/Extensions/QueryContextExtensions.cs b/src/Tgstation.Server.Host/Extensions/QueryContextExtensions.cs index 642f32cded..cd8e9e2ed0 100644 --- a/src/Tgstation.Server.Host/Extensions/QueryContextExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/QueryContextExtensions.cs @@ -12,6 +12,12 @@ namespace Tgstation.Server.Host.Extensions /// static class QueryContextExtensions { + /// + /// Translate a given into one with the target wrapped in an . + /// + /// The result of the . + /// The original . + /// A with the target wrapped in an . public static QueryContext> AuthorityResponseWrap(this QueryContext queryContext) { ArgumentNullException.ThrowIfNull(queryContext); @@ -32,7 +38,7 @@ namespace Tgstation.Server.Host.Extensions resultFromAuthority); var outerInvoke = Expression.Invoke( - AuthorityResponse.MappingExpression(), + AuthorityResponse.MappingExpression, innerInvoke); selector = Expression.Lambda, AuthorityResponse>>( @@ -60,6 +66,12 @@ namespace Tgstation.Server.Host.Extensions predicate); } + /// + /// Unwrap a given that was built from one resulting from . + /// + /// The underlying result of the . + /// The wrapped to unwrap. + /// The unwrapped for . public static QueryContext AuthorityResponseUnwrap(this QueryContext> queryContext) { ArgumentNullException.ThrowIfNull(queryContext); diff --git a/src/Tgstation.Server.Host/Extensions/QueryableExtensions.cs b/src/Tgstation.Server.Host/Extensions/QueryableExtensions.cs index ef63f0d8cc..a161dce091 100644 --- a/src/Tgstation.Server.Host/Extensions/QueryableExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/QueryableExtensions.cs @@ -16,7 +16,17 @@ namespace Tgstation.Server.Host.Extensions /// static class QueryableExtensions { - public static IQueryable> With(this IQueryable> queryable, QueryContext? queryContext) + /// + /// Map a given to project onto a given . + /// + /// The . + /// The . + /// A for s of /. + /// The for to map onto the . + /// A new with mapped on the . + public static IQueryable> With( + this IQueryable> queryable, + QueryContext? queryContext) { // taken verbatim from GreenDonut's With implementation ArgumentNullException.ThrowIfNull(queryable); @@ -28,14 +38,14 @@ namespace Tgstation.Server.Host.Extensions if (queryContext.Predicate != null) { queryable = queryable.Where( - TranslateGenericLambda( + TranslateLambda( queryContext.Predicate)); modified = true; } if (queryContext.Sorting?.Operations.Length > 0) { - var definition = new SortDefinition>( + var definition = new SortDefinition>( queryContext.Sorting.Operations .Select(MapSortBy)); @@ -45,19 +55,19 @@ namespace Tgstation.Server.Host.Extensions if (queryContext.Selector != null) { - Expression, TResult, Projected>> remapSelector = - (initialProjected, selectedResult) => new Projected + Expression, TResult, ProjectedPair>> remapSelector = + (initialProjected, selectedResult) => new ProjectedPair { Queried = initialProjected.Queried, Result = selectedResult, }; - Expression, TResult>> resultSelector = projected => projected.Result; - var parameter = Expression.Parameter(typeof(Projected), "projectedParam"); + Expression, TResult>> resultSelector = projected => projected.Result; + var parameter = Expression.Parameter(typeof(ProjectedPair), "projectedParam"); var result = Expression.Parameter(typeof(TResult), "resultParam"); var initialResultExpr = Expression.Invoke(resultSelector, parameter); var resultExpr = Expression.Invoke(queryContext.Selector, initialResultExpr); var finalInvoke = Expression.Invoke(remapSelector, parameter, resultExpr); - var finalExpr = Expression.Lambda, Projected>>(finalInvoke, parameter); + var finalExpr = Expression.Lambda, ProjectedPair>>(finalInvoke, parameter); queryable = queryable.Select(finalExpr); modified = true; @@ -70,15 +80,22 @@ namespace Tgstation.Server.Host.Extensions return queryable; } - private static ISortBy> MapSortBy(ISortBy sortBy) + /// + /// Map a given onto a . + /// + /// The . + /// The . + /// The for to map onto a of /. + /// A new for the . + private static ISortBy> MapSortBy(ISortBy sortBy) { var sortingKeyType = sortBy.GetType().GenericTypeArguments[1]; - var factoryMethod = typeof(SortBy>) + var factoryMethod = typeof(SortBy>) .GetMethod( sortBy.Ascending - ? nameof(SortBy>.Ascending) - : nameof(SortBy>.Descending))!; + ? nameof(SortBy>.Ascending) + : nameof(SortBy>.Descending))!; var instantiatedLambdaTranslate = typeof(QueryableExtensions) .GetMethod(nameof(TranslateLambda), BindingFlags.NonPublic | BindingFlags.Static)! @@ -88,21 +105,26 @@ namespace Tgstation.Server.Host.Extensions var instantiatedFactoryMethod = factoryMethod.MakeGenericMethod(sortingKeyType); - return (ISortBy>)instantiatedFactoryMethod.Invoke(null, [fixedLambda])!; + return (ISortBy>)instantiatedFactoryMethod.Invoke(null, [fixedLambda])!; } - private static Expression, TDesired>> TranslateGenericLambda(Expression> selectionExpression) - => TranslateLambda(selectionExpression); - - private static Expression, TDesired>> TranslateLambda(LambdaExpression selectionExpression) + /// + /// Translate a given on a onto its . + /// + /// The . + /// The . + /// The selected . + /// A accepting a and outputting a . + /// An accepting a of / and outputting a . + private static Expression, TDesired>> TranslateLambda(LambdaExpression selectionExpression) { - Expression, TResult>> resultSelector = projected => projected.Result; + Expression, TResult>> resultSelector = projected => projected.Result; - var parameter = Expression.Parameter(typeof(Projected), "projectedTranslateParam"); + var parameter = Expression.Parameter(typeof(ProjectedPair), "projectedTranslateParam"); var result = Expression.Invoke(resultSelector, parameter); var expr = Expression.Invoke(selectionExpression, result); - return Expression.Lambda, TDesired>>(expr, parameter); + return Expression.Lambda, TDesired>>(expr, parameter); } } } diff --git a/src/Tgstation.Server.Host/Extensions/RequestExecutorBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/RequestExecutorBuilderExtensions.cs index ba3554749a..73971968f0 100644 --- a/src/Tgstation.Server.Host/Extensions/RequestExecutorBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/RequestExecutorBuilderExtensions.cs @@ -20,6 +20,10 @@ namespace Tgstation.Server.Host.Extensions /// public static class RequestExecutorBuilderExtensions { + /// + /// Configure a GraphQL pipeline . + /// + /// The to configure. public static void ConfigureGraphQLServer(this IRequestExecutorBuilder builder) => (builder ?? throw new ArgumentNullException(nameof(builder))) .ModifyOptions(options => @@ -59,6 +63,10 @@ namespace Tgstation.Server.Host.Extensions .AddAuthorization() .ConfigureTypes(); + /// + /// Configure active s for a given . + /// + /// The to configure. private static void ConfigureTypes(this IRequestExecutorBuilder builder) => builder .AddQueryType() @@ -68,11 +76,15 @@ namespace Tgstation.Server.Host.Extensions .AddType() .AddType() .AddType() - .AddRightsHolders() .TryAddTypeInterceptor() - .BindRuntimeType(); + .BindRuntimeType() + .AddRightsHolders(); - private static IRequestExecutorBuilder AddRightsHolders(this IRequestExecutorBuilder builder) + /// + /// Configure s for all s. + /// + /// The to configure. + private static void AddRightsHolders(this IRequestExecutorBuilder builder) { var rightsHolderGeneric = typeof(RightsHolderType<>); foreach (var right in RightsHelper.AllRightTypes()) @@ -80,8 +92,6 @@ namespace Tgstation.Server.Host.Extensions var instantiatedRightsHolder = rightsHolderGeneric.MakeGenericType(right); builder.AddType(instantiatedRightsHolder); } - - return builder; } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Metadata/RightsHolderType{TRight}.cs b/src/Tgstation.Server.Host/GraphQL/Metadata/RightsHolderType{TRight}.cs index 504a1d172d..2d089e69b9 100644 --- a/src/Tgstation.Server.Host/GraphQL/Metadata/RightsHolderType{TRight}.cs +++ b/src/Tgstation.Server.Host/GraphQL/Metadata/RightsHolderType{TRight}.cs @@ -6,6 +6,10 @@ using Tgstation.Server.Host.GraphQL.Types; namespace Tgstation.Server.Host.GraphQL.Metadata { + /// + /// for s to give the flags proper GraphQL semantics. + /// + /// The of the . public sealed class RightsHolderType : ObjectType> where TRight : struct, Enum { diff --git a/src/Tgstation.Server.Host/GraphQL/Metadata/RightsTypeInterceptor.cs b/src/Tgstation.Server.Host/GraphQL/Metadata/RightsTypeInterceptor.cs index 502fb6bd7b..c40dec88dd 100644 --- a/src/Tgstation.Server.Host/GraphQL/Metadata/RightsTypeInterceptor.cs +++ b/src/Tgstation.Server.Host/GraphQL/Metadata/RightsTypeInterceptor.cs @@ -50,8 +50,7 @@ namespace Tgstation.Server.Host.GraphQL.Metadata /// /// Fix the "is" prefix on a given set of . /// - /// The of to correct. - /// The of s to operate on. + /// The of s to operate on. static void FixFields(IBindableList fields) { InputFieldDefinition? noneField = null; diff --git a/src/Tgstation.Server.Host/GraphQL/Types/ChatBot.cs b/src/Tgstation.Server.Host/GraphQL/Types/ChatBot.cs index 1fb56a494f..ad72301f53 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/ChatBot.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/ChatBot.cs @@ -1,7 +1,29 @@ -namespace Tgstation.Server.Host.GraphQL.Types +using System; +using System.Threading.Tasks; + +using HotChocolate.Types.Relay; + +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.GraphQL.Types { + /// + /// Represents a connection to a chat . + /// + [Node] public sealed class ChatBot : Entity { + /// + /// Node resolver for . + /// + /// The of the to retrieve. + /// The with if it exists, otherwise. + public static ValueTask GetChatBot(long id) + => throw new NotImplementedException(); + /// + /// The the chat bot uses. + /// + public required ChatProvider Provider { get; init; } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs b/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs index 2bd49bf45e..651ce1e00c 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/InstancePermissionSet.cs @@ -19,36 +19,36 @@ namespace Tgstation.Server.Host.GraphQL.Types /// /// The of the . /// - public RightsHolder InstancePermissionSetRights { get; set; } + public required RightsHolder InstancePermissionSetRights { get; set; } /// /// The of the . /// - public RightsHolder EngineRights { get; set; } + public required RightsHolder EngineRights { get; set; } /// /// The of the . /// - public RightsHolder DreamDaemonRights { get; set; } + public required RightsHolder DreamDaemonRights { get; set; } /// /// The of the . /// - public RightsHolder DreamMakerRights { get; set; } + public required RightsHolder DreamMakerRights { get; set; } /// /// The of the . /// - public RightsHolder RepositoryRights { get; set; } + public required RightsHolder RepositoryRights { get; set; } /// /// The of the . /// - public RightsHolder ChatBotRights { get; set; } + public required RightsHolder ChatBotRights { get; set; } /// /// The of the . /// - public RightsHolder ConfigurationRights { get; set; } + public required RightsHolder ConfigurationRights { get; set; } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/RightsHolder{TRight}.cs b/src/Tgstation.Server.Host/GraphQL/Types/RightsHolder{TRight}.cs index 8192396e82..2377afc9c2 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/RightsHolder{TRight}.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/RightsHolder{TRight}.cs @@ -2,20 +2,25 @@ namespace Tgstation.Server.Host.GraphQL.Types { + /// + /// Holder for a given . + /// + /// The being held. public sealed class RightsHolder where TRight : struct, Enum { - bool? contextFlag; - + /// + /// Marker property to allow to be properly selected by the GraphQL system. Should not be used for any other purpose. + /// public bool HasContextFlag { get => throw new InvalidOperationException($"{nameof(HasContextFlag)} getter should not be called!"); set => throw new InvalidOperationException($"{nameof(HasContextFlag)} setter should not be called!"); } + /// + /// The held value. + /// public required TRight Right { get; init; } - - public void SetContextFlag(TRight flag) - => contextFlag = Right.HasFlag(flag); } } diff --git a/src/Tgstation.Server.Host/GraphQL/Types/User.cs b/src/Tgstation.Server.Host/GraphQL/Types/User.cs index 7ebb2e77c3..1888f1b8cf 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/User.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/User.cs @@ -8,7 +8,6 @@ using GreenDonut.Data; using HotChocolate; using HotChocolate.Data; -using HotChocolate.Types; using HotChocolate.Types.Relay; using Tgstation.Server.Host.Authority; @@ -46,8 +45,14 @@ namespace Tgstation.Server.Host.GraphQL.Types /// public required string? SystemIdentifier { get; init; } + /// + /// The for the user. + /// public required UserGroup? Group { get; set; } + /// + /// The for the user if the user does not belong to a . + /// public required PermissionSet? OwnedPermissionSet { get; set; } /// @@ -62,6 +67,14 @@ namespace Tgstation.Server.Host.GraphQL.Types [IsProjected(true)] public required long? CreatedById { get; init; } + /// + /// Implements the . + /// + /// The of s to load. + /// The for the . + /// The for mapped to an . + /// The for the operation. + /// A resulting in a of the requested s. [DataLoader(AccessModifier = DataLoaderAccessModifier.PublicInterface)] public static ValueTask>> GetUsers( IReadOnlyList ids, diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs index defae0cfea..d08987d818 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserGroup.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using HotChocolate; -using HotChocolate.Authorization; using HotChocolate.Data; using HotChocolate.Types; using HotChocolate.Types.Relay; diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs index 18b7ae2f23..9ba7b79cf1 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UserName.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using HotChocolate; -using HotChocolate.Authorization; using HotChocolate.Types.Relay; using Tgstation.Server.Host.Authority; diff --git a/src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs b/src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs index e12ed7d488..1cf53baabc 100644 --- a/src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs +++ b/src/Tgstation.Server.Host/GraphQL/Types/UsersRepository.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using GreenDonut; using GreenDonut.Data; using HotChocolate; @@ -14,9 +12,7 @@ using HotChocolate.Types.Relay; using Microsoft.Extensions.Options; -using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Authority; -using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Models.Transformers; diff --git a/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs b/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs index b27292fb31..a845c8e78d 100644 --- a/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs +++ b/src/Tgstation.Server.Host/Models/ITransformer{TInput,TOutput}.cs @@ -18,9 +18,9 @@ namespace Tgstation.Server.Host.Models Expression> Expression { get; } /// - /// for mapping into a . + /// for mapping into a . /// - Expression>> ProjectedExpression { get; } + Expression>> ProjectedExpression { get; } /// /// The compiled . diff --git a/src/Tgstation.Server.Host/Models/Transformers/RightsHolderGraphQLTransformer{TRight}.cs b/src/Tgstation.Server.Host/Models/Transformers/RightsHolderGraphQLTransformer{TRight}.cs index 62dcba7c2d..a46cbfdd03 100644 --- a/src/Tgstation.Server.Host/Models/Transformers/RightsHolderGraphQLTransformer{TRight}.cs +++ b/src/Tgstation.Server.Host/Models/Transformers/RightsHolderGraphQLTransformer{TRight}.cs @@ -4,9 +4,16 @@ using Tgstation.Server.Host.GraphQL.Types; namespace Tgstation.Server.Host.Models.Transformers { + /// + /// for s. + /// + /// The of the . sealed class RightsHolderGraphQLTransformer : TransformerBase> where TRight : struct, Enum { + /// + /// Initializes a new instance of the class. + /// public RightsHolderGraphQLTransformer() : base(right => new RightsHolder { diff --git a/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TInput,TOutput}.cs b/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TInput,TOutput}.cs index 6832683ab3..318164c617 100644 --- a/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TInput,TOutput}.cs +++ b/src/Tgstation.Server.Host/Models/Transformers/TransformerBase{TInput,TOutput}.cs @@ -17,21 +17,35 @@ namespace Tgstation.Server.Host.Models.Transformers /// /// cache for . /// - static Expression>>? projectedExpression; + static Expression>>? projectedExpression; /// public Expression> Expression { get; } /// - public Expression>> ProjectedExpression { get; } + public Expression>> ProjectedExpression { get; } /// public Func CompiledExpression { get; } + /// + /// Gets the that should be used when a database projected non-null DTO value is null in the expression. + /// + /// The non-null a fallback is required for. + /// A fallback value. protected static T NotNullFallback() where T : notnull => default!; + /// + /// Build an for to when contains a with its own . + /// + /// The field in that needs transforming. + /// The transformed of . + /// The for /. + /// The to take a and and produce a . + /// The to select from . + /// An expression converting into based on with its other arguments generated from the transformation result of . protected static Expression> BuildSubProjection( Expression> transformerExpression, Expression> subInputSelectionExpression) @@ -60,6 +74,19 @@ namespace Tgstation.Server.Host.Models.Transformers return global::System.Linq.Expressions.Expression.Lambda>(outputExpression, primaryInput); } + /// + /// Build an for to when contains two sub-inputs with their own s. + /// + /// The first field in that needs transforming. + /// The second field in that needs transforming. + /// The first transformed of . + /// The second transformed of . + /// The for /. + /// The for /. + /// The to take a , , and and produce a . + /// The to select from . + /// The to select from . + /// An expression converting into based on with its other arguments generated from the transformation result of and . protected static Expression> BuildSubProjection< TSubInput1, TSubInput2, diff --git a/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs b/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs index 3009dd9f02..2a3656cc98 100644 --- a/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs +++ b/src/Tgstation.Server.Host/Models/Transformers/UserGraphQLTransformer.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; namespace Tgstation.Server.Host.Models.Transformers { @@ -30,20 +29,6 @@ namespace Tgstation.Server.Host.Models.Transformers Id = model.Id!.Value, Name = model.Name ?? NotNullFallback(), SystemIdentifier = model.SystemIdentifier, - OAuthConnections = model.OAuthConnections! - .Select(oAuthConnection => new GraphQL.Types.OAuth.OAuthConnection - { - Provider = oAuthConnection.Provider, - ExternalUserId = oAuthConnection.ExternalUserId!, - }) - .ToList(), - OidcConnections = model.OidcConnections! - .Select(oidcConnection => new GraphQL.Types.OAuth.OidcConnection - { - ExternalUserId = oidcConnection.ExternalUserId!, - SchemeKey = oidcConnection.SchemeKey!, - }) - .ToList(), OwnedPermissionSet = permissionSet, Group = group, }, diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 91dcae4c65..e5b0af46a8 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -1,4 +1,4 @@ - + @@ -6,7 +6,7 @@ $(TgsCoreVersion) true false - API1000;ASP0019 + API1000;ASP0019;CA1000 ClientApp/node_modules ClientApp/node_modules/.install-stamp enable