Fix build warnings

This commit is contained in:
Jordan Dominion
2025-08-15 16:31:13 -04:00
parent 6da893f8c9
commit 6ed93386b6
29 changed files with 334 additions and 157 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="myrules" Description="My rule set" ToolsVersion="17.0">
<Rules AnalyzerId="AsyncUsageAnalyzers" RuleNamespace="AsyncUsageAnalyzers">
<Rule Id="UseConfigureAwait" Action="Warning" />
</Rules>
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
<Rule Id="CA1000" Action="Warning" />
<Rule Id="CA1000" Action="None" />
<Rule Id="CA1001" Action="Warning" />
<Rule Id="CA1003" Action="Warning" />
<Rule Id="CA1004" Action="Warning" />
@@ -13,6 +13,11 @@ namespace Tgstation.Server.Host.Authority.Core
/// <typeparam name="TResult">The <see cref="Type"/> of success response.</typeparam>
public sealed class AuthorityResponse<TResult> : AuthorityResponse
{
/// <summary>
/// An expression to convert a <typeparamref name="TResult"/> into an <see cref="AuthorityResponse{TResult}"/>. The resulting <see cref="AuthorityResponse{TResult}"/> MUST NOT be used.
/// </summary>
public static Expression<Func<TResult, AuthorityResponse<TResult>>> MappingExpression { get; }
/// <inheritdoc />
[MemberNotNullWhen(true, nameof(IsNoContent))]
public override bool Success => base.Success;
@@ -34,11 +39,16 @@ namespace Tgstation.Server.Host.Authority.Core
/// </summary>
public HttpSuccessResponse? SuccessResponse { get; }
public static Expression<Func<TResult, AuthorityResponse<TResult>>> MappingExpression()
=> result => new AuthorityResponse<TResult>
/// <summary>
/// Initializes static members of the <see cref="AuthorityResponse{TResult}"/> class.
/// </summary>
static AuthorityResponse()
{
MappingExpression = result => new AuthorityResponse<TResult>
{
Result = result,
};
}
/// <summary>
/// Initializes a new instance of the <see cref="AuthorityResponse{TResult}"/> class.
@@ -14,8 +14,8 @@ namespace Tgstation.Server.Host.Authority.Core
/// <summary>
/// A projectable <typeparamref name="TResult"/> based on an underlying <typeparamref name="TQueried"/>.
/// </summary>
/// <typeparam name="TQueried">The DB model type queried.</typeparam>
/// <typeparam name="TResult">The transformed result.</typeparam>
/// <typeparam name="TQueried">The <see cref="EntityId"/> model <see cref="Type"/> queried.</typeparam>
/// <typeparam name="TResult">The transformed result <see cref="Type"/>.</typeparam>
public sealed class Projectable<TQueried, TResult>
where TQueried : EntityId
where TResult : notnull
@@ -26,14 +26,14 @@ namespace Tgstation.Server.Host.Authority.Core
readonly IQueryable<TQueried> query;
/// <summary>
/// The selector for the <see cref="Projected{TQueried, TResult}"/> data required by the <see cref="resultMapper"/>.
/// The selector for the <see cref="ProjectedPair{TQueried, TResult}"/> data required by the <see cref="resultMapper"/>.
/// </summary>
readonly Expression<Func<Projected<TQueried, TResult>, Projected<object?, TResult>>> selector;
readonly Expression<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>> selector;
/// <summary>
/// Mapper for transforming the <see cref="Projected{TQueried, TResult}"/> <typeparamref name="TResult"/> into an <see cref="AuthorityResponse{TResult}"/>. Called with the output of <see cref="selector"/> as <see cref="Projected{TQueried, TResult}.Queried"/>.
/// Mapper for transforming the <see cref="ProjectedPair{TQueried, TResult}"/> <typeparamref name="TResult"/> into an <see cref="AuthorityResponse{TResult}"/>. Called with the output of <see cref="selector"/> as <see cref="ProjectedPair{TQueried, TResult}.Queried"/>.
/// </summary>
readonly Func<Projected<object?, TResult>?, AuthorityResponse<TResult>> resultMapper;
readonly Func<ProjectedPair<object?, TResult>?, AuthorityResponse<TResult>> resultMapper;
/// <summary>
/// <see cref="CancellationToken"/> for the operation.
@@ -43,10 +43,10 @@ namespace Tgstation.Server.Host.Authority.Core
/// <summary>
/// Combine a set of <see cref="Projectable{TQueried, TResult}"/>s into the resulting <see cref="AuthorityResponse{TResult}"/>s.
/// </summary>
/// <param name="projection">The projection used to <see cref="Resolve(Func{IQueryable{TQueried}, IQueryable{Projected{TQueried, TResult}}})"/> each of the <paramref name="inputs"/>.</param>
/// <param name="projection">The projection used to <see cref="Resolve(Func{IQueryable{TQueried}, IQueryable{ProjectedPair{TQueried, TResult}}})"/> each of the <paramref name="inputs"/>.</param>
/// <param name="inputs">The <see cref="Projectable{TQueried, TResult}"/>s to combine.</param>
/// <returns>A <see cref="Dictionary{TKey, TValue}"/> of the resulting <see cref="AuthorityResponse{TResult}"/>s keyed by their <see cref="EntityId.Id"/>.</returns>
public static async ValueTask<Dictionary<long, AuthorityResponse<TResult>>> Combine(Func<IQueryable<TQueried>, IQueryable<Projected<TQueried, TResult>>> projection, params Projectable<TQueried, TResult>[] inputs)
public static async ValueTask<Dictionary<long, AuthorityResponse<TResult>>> Combine(Func<IQueryable<TQueried>, IQueryable<ProjectedPair<TQueried, TResult>>> projection, params Projectable<TQueried, TResult>[] inputs)
{
ArgumentNullException.ThrowIfNull(projection);
ArgumentNullException.ThrowIfNull(inputs);
@@ -76,13 +76,20 @@ namespace Tgstation.Server.Host.Authority.Core
result => firstProjectable.resultMapper(result.Projected));
}
/// <summary>
/// Create a <see cref="Projectable{TQueried, TResult}"/>.
/// </summary>
/// <param name="query">The underlying <see cref="IQueryable{T}"/>.</param>
/// <param name="resultMapper">A mapper for taking a new <see cref="ProjectedPair{TQueried, TResult}"/> (Who's <see cref="ProjectedPair{TQueried, TResult}.Queried"/> will be <see langword="null"/>) with its <see cref="ProjectedPair{TQueried, TResult}.Result"/> set amd converting it into an <see cref="AuthorityResponse{TResult}"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operations on <paramref name="query"/>.</param>
/// <returns>A new <see cref="Projectable{TQueried, TResult}"/>.</returns>
public static Projectable<TQueried, TResult> Create(
IQueryable<TQueried> query,
Func<Projected<object?, TResult>?, AuthorityResponse<TResult>> resultMapper,
Func<ProjectedPair<object?, TResult>?, AuthorityResponse<TResult>> resultMapper,
CancellationToken cancellationToken)
=> Create(
query,
projected => new Projected<object?, TResult>
projected => new ProjectedPair<object?, TResult>
{
Queried = null,
Result = projected.Result,
@@ -90,22 +97,31 @@ namespace Tgstation.Server.Host.Authority.Core
resultMapper,
cancellationToken);
/// <summary>
/// Create a <see cref="Projectable{TQueried, TResult}"/>.
/// </summary>
/// <typeparam name="TSelection">A <see cref="Type"/> selected out of <typeparamref name="TQueried"/> to be used in <paramref name="resultMapper"/>.</typeparam>
/// <param name="query">The underlying <see cref="IQueryable{T}"/>.</param>
/// <param name="selector">Selects a value to be set as the <see cref="ProjectedPair{TQueried, TResult}.Queried"/> value for later use in <paramref name="resultMapper"/>.</param>
/// <param name="resultMapper">A mapper for taking a new <see cref="ProjectedPair{TQueried, TResult}"/> (Who's <see cref="ProjectedPair{TQueried, TResult}.Queried"/> will be the result of <paramref name="selector"/> on the original <paramref name="query"/>.) with its <see cref="ProjectedPair{TQueried, TResult}.Result"/> set amd converting it into an <see cref="AuthorityResponse{TResult}"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operations on <paramref name="query"/>.</param>
/// <returns>A new <see cref="Projectable{TQueried, TResult}"/>.</returns>
public static Projectable<TQueried, TResult> Create<TSelection>(
IQueryable<TQueried> query,
Expression<Func<Projected<TQueried, TResult>, Projected<TSelection, TResult>>> selector,
Func<Projected<TSelection, TResult>?, AuthorityResponse<TResult>> resultMapper,
Expression<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<TSelection, TResult>>> selector,
Func<ProjectedPair<TSelection, TResult>?, AuthorityResponse<TResult>> resultMapper,
CancellationToken cancellationToken)
{
Expression<Func<Projected<TSelection, TResult>, Projected<object?, TResult>>> makeProjectedGeneric = projected => new Projected<object?, TResult>
Expression<Func<ProjectedPair<TSelection, TResult>, ProjectedPair<object?, TResult>>> makeProjectedGeneric = projected => new ProjectedPair<object?, TResult>
{
Queried = projected.Queried,
Result = projected.Result,
};
var parameter = Expression.Parameter(typeof(Projected<TQueried, TResult>), "innerProjectedParam");
var parameter = Expression.Parameter(typeof(ProjectedPair<TQueried, TResult>), "innerProjectedParam");
return new(
query,
Expression.Lambda<Func<Projected<TQueried, TResult>, Projected<object?, TResult>>>(
Expression.Lambda<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>>(
Expression.Invoke(
makeProjectedGeneric,
Expression.Invoke(
@@ -114,7 +130,7 @@ namespace Tgstation.Server.Host.Authority.Core
parameter),
projected => resultMapper(
projected != null
? new Projected<TSelection, TResult>
? new ProjectedPair<TSelection, TResult>
{
Queried = (TSelection)projected.Queried!,
Result = projected.Result,
@@ -124,43 +140,15 @@ namespace Tgstation.Server.Host.Authority.Core
}
/// <summary>
/// Initializes a new instance of the <see cref="Projectable{TQueried, TResult}"/> class.
/// Create an <see cref="Expression{TDelegate}"/> to transform a <see cref="ProjectedPair{TQueried, TResult}"/> into a <see cref="CombinedProjection"/>.
/// </summary>
/// <param name="query">The value of <see cref="query"/>.</param>
/// <param name="selector">The value of <see cref="selector"/>.</param>
/// <param name="resultMapper">The value of <see cref="resultMapper"/>.</param>
/// <param name="cancellationToken">The value of <see cref="cancellationToken"/>.</param>
private Projectable(
IQueryable<TQueried> query,
Expression<Func<Projected<TQueried, TResult>, Projected<object?, TResult>>> selector,
Func<Projected<object?, TResult>?, AuthorityResponse<TResult>> resultMapper,
CancellationToken cancellationToken)
/// <param name="selector">The selector used for to get business data for the <see cref="resultMapper"/>.</param>
/// <returns>A new <see cref="Expression{TDelegate}"/> transforming a <see cref="ProjectedPair{TQueried, TResult}"/> into a <see cref="CombinedProjection"/>.</returns>
private static Expression<Func<ProjectedPair<TQueried, TResult>, CombinedProjection>> CombinedProjectionExpression(Expression<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>> 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<Func<ProjectedPair<TQueried, TResult>, long>> idSelector = projected => projected.Queried.Id!.Value;
/// <summary>
/// Resolve the <see cref="Projectable{TQueried, TResult}"/>.
/// </summary>
/// <param name="projection">The mapping from <typeparamref name="TQueried"/> to <typeparamref name="TResult"/>.</param>
/// <returns>The resolved <see cref="AuthorityResponse{TResult}"/>.</returns>
public async ValueTask<AuthorityResponse<TResult>> Resolve(Func<IQueryable<TQueried>, IQueryable<Projected<TQueried, TResult>>> projection)
{
ArgumentNullException.ThrowIfNull(projection);
var selection = await projection(query)
.Select(selector)
.FirstOrDefaultAsync(cancellationToken);
return resultMapper(selection);
}
private static Expression<Func<Projected<TQueried, TResult>, CombinedProjection>> CombinedProjectionExpression(Expression<Func<Projected<TQueried, TResult>, Projected<object?, TResult>>> selector)
{
Expression<Func<Projected<TQueried, TResult>, long>> idSelector = projected => projected.Queried.Id!.Value;
var parameter = Expression.Parameter(typeof(Projected<TQueried, TResult>), "projectedParamForCombined");
var parameter = Expression.Parameter(typeof(ProjectedPair<TQueried, TResult>), "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<Func<Projected<TQueried, TResult>, CombinedProjection>>(memberInitExpr, parameter);
var finalExpr = Expression.Lambda<Func<ProjectedPair<TQueried, TResult>, CombinedProjection>>(memberInitExpr, parameter);
return finalExpr;
}
public class CombinedProjection
/// <summary>
/// Initializes a new instance of the <see cref="Projectable{TQueried, TResult}"/> class.
/// </summary>
/// <param name="query">The value of <see cref="query"/>.</param>
/// <param name="selector">The value of <see cref="selector"/>.</param>
/// <param name="resultMapper">The value of <see cref="resultMapper"/>.</param>
/// <param name="cancellationToken">The value of <see cref="cancellationToken"/>.</param>
private Projectable(
IQueryable<TQueried> query,
Expression<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<object?, TResult>>> selector,
Func<ProjectedPair<object?, TResult>?, AuthorityResponse<TResult>> 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<object?, TResult> Projected { get; init; }
/// <summary>
/// Resolve the <see cref="Projectable{TQueried, TResult}"/>.
/// </summary>
/// <param name="projection">The mapping from <typeparamref name="TQueried"/> to <typeparamref name="TResult"/>.</param>
/// <returns>The resolved <see cref="AuthorityResponse{TResult}"/>.</returns>
public async ValueTask<AuthorityResponse<TResult>> Resolve(Func<IQueryable<TQueried>, IQueryable<ProjectedPair<TQueried, TResult>>> projection)
{
ArgumentNullException.ThrowIfNull(projection);
var selection = await projection(query)
.Select(selector)
.FirstOrDefaultAsync(cancellationToken);
return resultMapper(selection);
}
/// <summary>
/// DTO for selecting the <see cref="Id"/> from <typeparamref name="TQueried"/> alongside the selection <see cref="ProjectedPair{TQueried, TResult}"/>.
/// </summary>
private class CombinedProjection
{
/// <summary>
/// The <see cref="EntityId.Id"/> of <typeparamref name="TQueried"/>.
/// </summary>
public required long Id { get; init; }
/// <summary>
/// The selection/<typeparamref name="TResult"/> <see cref="ProjectedPair{TQueried, TResult}"/>.
/// </summary>
public required ProjectedPair<object?, TResult> Projected { get; init; }
}
}
}
@@ -0,0 +1,20 @@
namespace Tgstation.Server.Host.Authority.Core
{
/// <summary>
/// DTO for moving database projected <see cref="object"/>s through the system.
/// </summary>
/// <typeparam name="TQueried">The originally queried <see cref="global::System.Type"/>.</typeparam>
/// <typeparam name="TResult">The output DTO <see cref="global::System.Type"/>.</typeparam>
public sealed class ProjectedPair<TQueried, TResult>
{
/// <summary>
/// The originally queried <typeparamref name="TQueried"/>.
/// </summary>
public required TQueried Queried { get; init; }
/// <summary>
/// The output DTO <typeparamref name="TResult"/>.
/// </summary>
public required TResult Result { get; init; }
}
}
@@ -1,9 +0,0 @@
namespace Tgstation.Server.Host.Authority.Core
{
public class Projected<TQueried, TResult>
{
public TQueried Queried { get; init; }
public TResult Result { get; init; }
}
}
@@ -40,9 +40,7 @@ namespace Tgstation.Server.Host.Authority.Core
/// </summary>
/// <param name="result">The <typeparamref name="TResult"/> to convert.</param>
/// <returns>A new <see cref="RequirementsGated{TResult}"/> based on <paramref name="result"/>.</returns>
#pragma warning disable CA1000 // Do not declare static members on generic types
public static RequirementsGated<TResult> FromResult(TResult result)
#pragma warning restore CA1000 // Do not declare static members on generic types
=> new(
() => (IAuthorizationRequirement?)null,
() => ValueTask.FromResult(result));
@@ -91,6 +91,16 @@ namespace Tgstation.Server.Host.Authority
where TApiModel : notnull
where TTransformer : ITransformer<TResult, TApiModel>, new();
/// <summary>
/// Execute a batch loaded call on a given <paramref name="authorityInvoker"/>.
/// </summary>
/// <typeparam name="TResult">The queried result of 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="RequirementsGated{TResult}"/> <see cref="Projectable{TQueried, TResult}"/> from <typeparamref name="TResult"/> to <typeparamref name="TApiModel"/>.</param>
/// <param name="ids">The list of <see cref="EntityId.Id"/>s to batch load.</param>
/// <param name="queryContext">The active <see cref="QueryContext{TEntity}"/> mapped to an <see cref="AuthorityResponse{TResult}"/> of the <typeparamref name="TApiModel"/>. MUST have been wrapped with <see cref="Extensions.QueryContextExtensions.AuthorityResponseWrap{TResult}(QueryContext{TResult})"/>. This will be unwrapped so only a <see cref="QueryContext{TEntity}"/> of <typeparamref name="TApiModel"/> will be used.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="Dictionary{TKey, TValue}"/> of loaded <see cref="EntityId.Id"/>s to <see cref="AuthorityResponse{TResult}"/>s for the loaded <typeparamref name="TApiModel"/>s.</returns>
ValueTask<Dictionary<long, AuthorityResponse<TApiModel>>> ExecuteDataLoader<TResult, TApiModel, TTransformer>(
Func<TAuthority, long, RequirementsGated<Projectable<TResult, TApiModel>>> authorityInvoker,
IReadOnlyList<long> ids,
@@ -30,6 +30,14 @@ namespace Tgstation.Server.Host.Authority
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
RequirementsGated<AuthorityResponse<User>> GetId(long id, bool includeJoins, bool allowSystemUser, CancellationToken cancellationToken);
/// <summary>
/// Gets the <see cref="User"/> with a given <paramref name="id"/>.
/// </summary>
/// <typeparam name="TResult">The result type after projection.</typeparam>
/// <param name="id">The <see cref="EntityId.Id"/> of the <see cref="User"/>.</param>
/// <param name="allowSystemUser">If the <see cref="User.TgsSystemUserName"/> may be returned.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="RequirementsGated{TResult}"/> <see cref="Projectable{TQueried, TResult}"/> <see cref="User"/> for <typeparamref name="TResult"/>.</returns>
RequirementsGated<Projectable<User, TResult>> GetId<TResult>(long id, bool allowSystemUser, CancellationToken cancellationToken)
where TResult : class;
@@ -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<string, TResult>
projected => new ProjectedPair<string, TResult>
{
Queried = projected.Queried.CanonicalName!,
Result = projected.Result,
@@ -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<GraphQL.Subscriptions.ITopicEventReceiver, ShutdownAwareTopicEventReceiver>()
.AddScoped<ITopicEventReceiver, ShutdownAwareTopicEventReceiver>()
.AddGraphQLServer()
.ConfigureGraphQLServer();
@@ -9,15 +9,26 @@ using Tgstation.Server.Host.Authority.Core;
namespace Tgstation.Server.Host.Extensions
{
/// <summary>
/// Extension methods for <see cref="IDataLoader{TKey, TValue}"/>.
/// </summary>
static class DataLoaderExtensions
{
public static async ValueTask<TResult?> LoadAuthorityResponse<TResult, TID>(
this IDataLoader<TID, AuthorityResponse<TResult>> dataLoader,
/// <summary>
/// Convert a request for a single <typeparamref name="TResult"/> into a data-loader invocation for the matching <see cref="AuthorityResponse{TResult}"/>.
/// </summary>
/// <typeparam name="TResult">The <see cref="Type"/> of the underlying DTO being loaded.</typeparam>
/// <param name="dataLoader">The <see cref="IDataLoader{TKey, TValue}"/> for <typeparamref name="TResult"/> <see cref="AuthorityResponse{TResult}"/>s.</param>
/// <param name="queryContext">The active <see cref="QueryContext{TEntity}"/>, if any.</param>
/// <param name="id">The <see cref="Api.Models.EntityId.Id"/> of <typeparamref name="TResult"/> to load.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the loaded <typeparamref name="TResult"/> if it exists, <see langword="null"/> otherwise.</returns>
public static async ValueTask<TResult?> LoadAuthorityResponse<TResult>(
this IDataLoader<long, AuthorityResponse<TResult>> dataLoader,
QueryContext<TResult>? queryContext,
TID id,
long id,
CancellationToken cancellationToken)
where TResult : class
where TID : notnull
{
ArgumentNullException.ThrowIfNull(dataLoader);
@@ -10,23 +10,30 @@ namespace Tgstation.Server.Host.Extensions
/// </summary>
static class ExpressionExtensions
{
public static Expression<Func<TQueried, Projected<TQueried, TResult>>> Projected<TQueried, TResult>(this Expression<Func<TQueried, TResult>> translationExpression)
/// <summary>
/// Create an <see cref="Expression{TDelegate}"/> to transform a given <typeparamref name="TQueried"/> into a <typeparamref name="TResult"/> and output them as a <see cref="ProjectedPair{TQueried, TResult}"/>.
/// </summary>
/// <typeparam name="TQueried">The input <see cref="Type"/>.</typeparam>
/// <typeparam name="TResult">The output <see cref="Type"/>.</typeparam>
/// <param name="translationExpression">An <see cref="Expression{TDelegate}"/> to transform a given <typeparamref name="TQueried"/> into a <typeparamref name="TResult"/>.</param>
/// <returns>An <see cref="Expression{TDelegate}"/> to transform a given <typeparamref name="TQueried"/> into a <typeparamref name="TResult"/> and output them as a <see cref="ProjectedPair{TQueried, TResult}"/>.</returns>
public static Expression<Func<TQueried, ProjectedPair<TQueried, TResult>>> Projected<TQueried, TResult>(this Expression<Func<TQueried, TResult>> translationExpression)
{
var parameter = Expression.Parameter(typeof(TQueried), "queried");
var body = Expression.Invoke(translationExpression, parameter);
var ourType = typeof(Projected<TQueried, TResult>);
var ourType = typeof(ProjectedPair<TQueried, TResult>);
var expr = Expression.MemberInit(
Expression.New(ourType),
Expression.Bind(
ourType.GetProperty(nameof(Authority.Core.Projected<TQueried, TResult>.Queried))!,
ourType.GetProperty(nameof(ProjectedPair<TQueried, TResult>.Queried))!,
parameter),
Expression.Bind(
ourType.GetProperty(nameof(Authority.Core.Projected<TQueried, TResult>.Result))!,
ourType.GetProperty(nameof(ProjectedPair<TQueried, TResult>.Result))!,
body));
return Expression.Lambda<Func<TQueried, Projected<TQueried, TResult>>>(expr, parameter);
return Expression.Lambda<Func<TQueried, ProjectedPair<TQueried, TResult>>>(expr, parameter);
}
}
}
@@ -12,6 +12,12 @@ namespace Tgstation.Server.Host.Extensions
/// </summary>
static class QueryContextExtensions
{
/// <summary>
/// Translate a given <paramref name="queryContext"/> into one with the target wrapped in an <see cref="AuthorityResponse{TResult}"/>.
/// </summary>
/// <typeparam name="TResult">The result <see cref="Type"/> of the <paramref name="queryContext"/>.</typeparam>
/// <param name="queryContext">The original <see cref="QueryContext{TEntity}"/>.</param>
/// <returns>A <see cref="QueryContext{TEntity}"/> with the target wrapped in an <see cref="AuthorityResponse{TResult}"/>.</returns>
public static QueryContext<AuthorityResponse<TResult>> AuthorityResponseWrap<TResult>(this QueryContext<TResult> queryContext)
{
ArgumentNullException.ThrowIfNull(queryContext);
@@ -32,7 +38,7 @@ namespace Tgstation.Server.Host.Extensions
resultFromAuthority);
var outerInvoke = Expression.Invoke(
AuthorityResponse<TResult>.MappingExpression(),
AuthorityResponse<TResult>.MappingExpression,
innerInvoke);
selector = Expression.Lambda<Func<AuthorityResponse<TResult>, AuthorityResponse<TResult>>>(
@@ -60,6 +66,12 @@ namespace Tgstation.Server.Host.Extensions
predicate);
}
/// <summary>
/// Unwrap a given <paramref name="queryContext"/> that was built from one resulting from <see cref="AuthorityResponseWrap{TResult}(QueryContext{TResult})"/>.
/// </summary>
/// <typeparam name="TResult">The underlying result <see cref="Type"/> of the <paramref name="queryContext"/>.</typeparam>
/// <param name="queryContext">The wrapped <see cref="QueryContext{TEntity}"/> to unwrap.</param>
/// <returns>The unwrapped <see cref="QueryContext{TEntity}"/> for <typeparamref name="TResult"/>.</returns>
public static QueryContext<TResult> AuthorityResponseUnwrap<TResult>(this QueryContext<AuthorityResponse<TResult>> queryContext)
{
ArgumentNullException.ThrowIfNull(queryContext);
@@ -16,7 +16,17 @@ namespace Tgstation.Server.Host.Extensions
/// </summary>
static class QueryableExtensions
{
public static IQueryable<Projected<TQueried, TResult>> With<TQueried, TResult>(this IQueryable<Projected<TQueried, TResult>> queryable, QueryContext<TResult>? queryContext)
/// <summary>
/// Map a given <paramref name="queryContext"/> to project onto a given <paramref name="queryable"/>.
/// </summary>
/// <typeparam name="TQueried">The <see cref="ProjectedPair{TQueried, TResult}.Queried"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TResult">The <see cref="ProjectedPair{TQueried, TResult}.Result"/> <see cref="Type"/>.</typeparam>
/// <param name="queryable">A <see cref="IQueryable{T}"/> for <see cref="ProjectedPair{TQueried, TResult}"/>s of <typeparamref name="TQueried"/>/<typeparamref name="TResult"/>.</param>
/// <param name="queryContext">The <see cref="QueryContext{TEntity}"/> for <typeparamref name="TResult"/> to map onto the <paramref name="queryable"/>.</param>
/// <returns>A new <see cref="IQueryable{T}"/> with <paramref name="queryContext"/> mapped on the <typeparamref name="TResult"/>.</returns>
public static IQueryable<ProjectedPair<TQueried, TResult>> With<TQueried, TResult>(
this IQueryable<ProjectedPair<TQueried, TResult>> queryable,
QueryContext<TResult>? 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<TQueried, TResult, bool>(
TranslateLambda<TQueried, TResult, bool>(
queryContext.Predicate));
modified = true;
}
if (queryContext.Sorting?.Operations.Length > 0)
{
var definition = new SortDefinition<Projected<TQueried, TResult>>(
var definition = new SortDefinition<ProjectedPair<TQueried, TResult>>(
queryContext.Sorting.Operations
.Select(MapSortBy<TQueried, TResult>));
@@ -45,19 +55,19 @@ namespace Tgstation.Server.Host.Extensions
if (queryContext.Selector != null)
{
Expression<Func<Projected<TQueried, TResult>, TResult, Projected<TQueried, TResult>>> remapSelector =
(initialProjected, selectedResult) => new Projected<TQueried, TResult>
Expression<Func<ProjectedPair<TQueried, TResult>, TResult, ProjectedPair<TQueried, TResult>>> remapSelector =
(initialProjected, selectedResult) => new ProjectedPair<TQueried, TResult>
{
Queried = initialProjected.Queried,
Result = selectedResult,
};
Expression<Func<Projected<TQueried, TResult>, TResult>> resultSelector = projected => projected.Result;
var parameter = Expression.Parameter(typeof(Projected<TQueried, TResult>), "projectedParam");
Expression<Func<ProjectedPair<TQueried, TResult>, TResult>> resultSelector = projected => projected.Result;
var parameter = Expression.Parameter(typeof(ProjectedPair<TQueried, TResult>), "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<Func<Projected<TQueried, TResult>, Projected<TQueried, TResult>>>(finalInvoke, parameter);
var finalExpr = Expression.Lambda<Func<ProjectedPair<TQueried, TResult>, ProjectedPair<TQueried, TResult>>>(finalInvoke, parameter);
queryable = queryable.Select(finalExpr);
modified = true;
@@ -70,15 +80,22 @@ namespace Tgstation.Server.Host.Extensions
return queryable;
}
private static ISortBy<Projected<TQueried, TResult>> MapSortBy<TQueried, TResult>(ISortBy<TResult> sortBy)
/// <summary>
/// Map a given <paramref name="sortBy"/> onto a <see cref="ProjectedPair{TQueried, TResult}"/>.
/// </summary>
/// <typeparam name="TQueried">The <see cref="ProjectedPair{TQueried, TResult}.Queried"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TResult">The <see cref="ProjectedPair{TQueried, TResult}.Result"/> <see cref="Type"/>.</typeparam>
/// <param name="sortBy">The <see cref="ISortBy{TEntity}"/> for <typeparamref name="TResult"/> to map onto a <see cref="ProjectedPair{TQueried, TResult}"/> of <typeparamref name="TQueried"/>/<typeparamref name="TResult"/>.</param>
/// <returns>A new <see cref="ISortBy{TEntity}"/> for the <see cref="ProjectedPair{TQueried, TResult}"/>.</returns>
private static ISortBy<ProjectedPair<TQueried, TResult>> MapSortBy<TQueried, TResult>(ISortBy<TResult> sortBy)
{
var sortingKeyType = sortBy.GetType().GenericTypeArguments[1];
var factoryMethod = typeof(SortBy<Projected<TQueried, TResult>>)
var factoryMethod = typeof(SortBy<ProjectedPair<TQueried, TResult>>)
.GetMethod(
sortBy.Ascending
? nameof(SortBy<Projected<TQueried, TResult>>.Ascending)
: nameof(SortBy<Projected<TQueried, TResult>>.Descending))!;
? nameof(SortBy<ProjectedPair<TQueried, TResult>>.Ascending)
: nameof(SortBy<ProjectedPair<TQueried, TResult>>.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<Projected<TQueried, TResult>>)instantiatedFactoryMethod.Invoke(null, [fixedLambda])!;
return (ISortBy<ProjectedPair<TQueried, TResult>>)instantiatedFactoryMethod.Invoke(null, [fixedLambda])!;
}
private static Expression<Func<Projected<TQueried, TResult>, TDesired>> TranslateGenericLambda<TQueried, TResult, TDesired>(Expression<Func<TResult, TDesired>> selectionExpression)
=> TranslateLambda<TQueried, TResult, TDesired>(selectionExpression);
private static Expression<Func<Projected<TQueried, TResult>, TDesired>> TranslateLambda<TQueried, TResult, TDesired>(LambdaExpression selectionExpression)
/// <summary>
/// Translate a given <paramref name="selectionExpression"/> on a <typeparamref name="TResult"/> onto its <see cref="ProjectedPair{TQueried, TResult}"/>.
/// </summary>
/// <typeparam name="TQueried">The <see cref="ProjectedPair{TQueried, TResult}.Queried"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TResult">The <see cref="ProjectedPair{TQueried, TResult}.Result"/> <see cref="Type"/>.</typeparam>
/// <typeparam name="TDesired">The selected <see cref="Type"/>.</typeparam>
/// <param name="selectionExpression">A <see cref="LambdaExpression"/> accepting a <typeparamref name="TResult"/> and outputting a <typeparamref name="TDesired"/>.</param>
/// <returns>An <see cref="Expression{TDelegate}"/> accepting a <see cref="ProjectedPair{TQueried, TResult}"/> of <typeparamref name="TQueried"/>/<typeparamref name="TResult"/> and outputting a <typeparamref name="TDesired"/>.</returns>
private static Expression<Func<ProjectedPair<TQueried, TResult>, TDesired>> TranslateLambda<TQueried, TResult, TDesired>(LambdaExpression selectionExpression)
{
Expression<Func<Projected<TQueried, TResult>, TResult>> resultSelector = projected => projected.Result;
Expression<Func<ProjectedPair<TQueried, TResult>, TResult>> resultSelector = projected => projected.Result;
var parameter = Expression.Parameter(typeof(Projected<TQueried, TResult>), "projectedTranslateParam");
var parameter = Expression.Parameter(typeof(ProjectedPair<TQueried, TResult>), "projectedTranslateParam");
var result = Expression.Invoke(resultSelector, parameter);
var expr = Expression.Invoke(selectionExpression, result);
return Expression.Lambda<Func<Projected<TQueried, TResult>, TDesired>>(expr, parameter);
return Expression.Lambda<Func<ProjectedPair<TQueried, TResult>, TDesired>>(expr, parameter);
}
}
}
@@ -20,6 +20,10 @@ namespace Tgstation.Server.Host.Extensions
/// </summary>
public static class RequestExecutorBuilderExtensions
{
/// <summary>
/// Configure a GraphQL pipeline <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IRequestExecutorBuilder"/> to configure.</param>
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();
/// <summary>
/// Configure active <see cref="Type"/>s for a given <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IRequestExecutorBuilder"/> to configure.</param>
private static void ConfigureTypes(this IRequestExecutorBuilder builder)
=> builder
.AddQueryType<Query>()
@@ -68,11 +76,15 @@ namespace Tgstation.Server.Host.Extensions
.AddType<LocalGateway>()
.AddType<UserName>()
.AddType<UnsignedIntType>()
.AddRightsHolders()
.TryAddTypeInterceptor<RightsTypeInterceptor>()
.BindRuntimeType<Version, SemverType>();
.BindRuntimeType<Version, SemverType>()
.AddRightsHolders();
private static IRequestExecutorBuilder AddRightsHolders(this IRequestExecutorBuilder builder)
/// <summary>
/// Configure <see cref="RightsHolderType{TRight}"/>s for all <see cref="RightsType"/>s.
/// </summary>
/// <param name="builder">The <see cref="IRequestExecutorBuilder"/> to configure.</param>
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;
}
}
}
@@ -6,6 +6,10 @@ using Tgstation.Server.Host.GraphQL.Types;
namespace Tgstation.Server.Host.GraphQL.Metadata
{
/// <summary>
/// <see cref="ObjectType{T}"/> for <see cref="RightsHolder{TRight}"/>s to give the flags proper GraphQL semantics.
/// </summary>
/// <typeparam name="TRight">The <see cref="Api.Rights.RightsType"/> of the <see cref="RightsHolder{TRight}"/>.</typeparam>
public sealed class RightsHolderType<TRight> : ObjectType<RightsHolder<TRight>>
where TRight : struct, Enum
{
@@ -50,8 +50,7 @@ namespace Tgstation.Server.Host.GraphQL.Metadata
/// <summary>
/// Fix the "is" prefix on a given set of <paramref name="fields"/>.
/// </summary>
/// <typeparam name="TField">The <see cref="Type"/> of <see cref="FieldDefinitionBase"/> to correct.</typeparam>
/// <param name="fields">The <see cref="IBindableList{T}"/> of <typeparamref name="TField"/>s to operate on.</param>
/// <param name="fields">The <see cref="IBindableList{T}"/> of <see cref="InputFieldDefinition"/>s to operate on.</param>
static void FixFields(IBindableList<InputFieldDefinition> fields)
{
InputFieldDefinition? noneField = null;
@@ -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
{
/// <summary>
/// Represents a connection to a chat <see cref="Provider"/>.
/// </summary>
[Node]
public sealed class ChatBot : Entity
{
/// <summary>
/// Node resolver for <see cref="ChatBot"/>.
/// </summary>
/// <param name="id">The <see cref="Entity.Id"/> of the <see cref="ChatBot"/> to retrieve.</param>
/// <returns>The <see cref="ChatBot"/> with <paramref name="id"/> if it exists, <see langword="null"/> otherwise.</returns>
public static ValueTask<ChatBot?> GetChatBot(long id)
=> throw new NotImplementedException();
/// <summary>
/// The <see cref="ChatProvider"/> the chat bot uses.
/// </summary>
public required ChatProvider Provider { get; init; }
}
}
@@ -19,36 +19,36 @@ namespace Tgstation.Server.Host.GraphQL.Types
/// <summary>
/// The <see cref="Api.Rights.InstancePermissionSetRights"/> of the <see cref="InstancePermissionSet"/>.
/// </summary>
public RightsHolder<InstancePermissionSetRights> InstancePermissionSetRights { get; set; }
public required RightsHolder<InstancePermissionSetRights> InstancePermissionSetRights { get; set; }
/// <summary>
/// The <see cref="Api.Rights.EngineRights"/> of the <see cref="InstancePermissionSet"/>.
/// </summary>
public RightsHolder<EngineRights> EngineRights { get; set; }
public required RightsHolder<EngineRights> EngineRights { get; set; }
/// <summary>
/// The <see cref="Api.Rights.DreamDaemonRights"/> of the <see cref="InstancePermissionSet"/>.
/// </summary>
public RightsHolder<DreamDaemonRights> DreamDaemonRights { get; set; }
public required RightsHolder<DreamDaemonRights> DreamDaemonRights { get; set; }
/// <summary>
/// The <see cref="Api.Rights.DreamMakerRights"/> of the <see cref="InstancePermissionSet"/>.
/// </summary>
public RightsHolder<DreamMakerRights> DreamMakerRights { get; set; }
public required RightsHolder<DreamMakerRights> DreamMakerRights { get; set; }
/// <summary>
/// The <see cref="Api.Rights.RepositoryRights"/> of the <see cref="InstancePermissionSet"/>.
/// </summary>
public RightsHolder<RepositoryRights> RepositoryRights { get; set; }
public required RightsHolder<RepositoryRights> RepositoryRights { get; set; }
/// <summary>
/// The <see cref="Api.Rights.ChatBotRights"/> of the <see cref="InstancePermissionSet"/>.
/// </summary>
public RightsHolder<ChatBotRights> ChatBotRights { get; set; }
public required RightsHolder<ChatBotRights> ChatBotRights { get; set; }
/// <summary>
/// The <see cref="Api.Rights.ConfigurationRights"/> of the <see cref="InstancePermissionSet"/>.
/// </summary>
public RightsHolder<ConfigurationRights> ConfigurationRights { get; set; }
public required RightsHolder<ConfigurationRights> ConfigurationRights { get; set; }
}
}
@@ -2,20 +2,25 @@
namespace Tgstation.Server.Host.GraphQL.Types
{
/// <summary>
/// Holder for a given <typeparamref name="TRight"/>.
/// </summary>
/// <typeparam name="TRight">The <see cref="Api.Rights.RightsType"/> being held.</typeparam>
public sealed class RightsHolder<TRight>
where TRight : struct, Enum
{
bool? contextFlag;
/// <summary>
/// Marker property to allow <see cref="Right"/> to be properly selected by the GraphQL system. Should not be used for any other purpose.
/// </summary>
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!");
}
/// <summary>
/// The held <typeparamref name="TRight"/> value.
/// </summary>
public required TRight Right { get; init; }
public void SetContextFlag(TRight flag)
=> contextFlag = Right.HasFlag(flag);
}
}
@@ -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
/// </summary>
public required string? SystemIdentifier { get; init; }
/// <summary>
/// The <see cref="UserGroup"/> for the user.
/// </summary>
public required UserGroup? Group { get; set; }
/// <summary>
/// The <see cref="PermissionSet"/> for the user if the user does not belong to a <see cref="Group"/>.
/// </summary>
public required PermissionSet? OwnedPermissionSet { get; set; }
/// <summary>
@@ -62,6 +67,14 @@ namespace Tgstation.Server.Host.GraphQL.Types
[IsProjected(true)]
public required long? CreatedById { get; init; }
/// <summary>
/// Implements the <see cref="IUserGroupsDataLoader"/>.
/// </summary>
/// <param name="ids">The <see cref="IReadOnlyList{T}"/> of <see cref="User"/> <see cref="Api.Models.EntityId.Id"/>s to load.</param>
/// <param name="userAuthority">The <see cref="IGraphQLAuthorityInvoker{TAuthority}"/> for the <see cref="IUserAuthority"/>.</param>
/// <param name="queryContext">The <see cref="QueryContext{TEntity}"/> for <see cref="User"/> mapped to an <see cref="AuthorityResponse{TResult}"/>.</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"/> <see cref="AuthorityResponse{TResult}"/>s.</returns>
[DataLoader(AccessModifier = DataLoaderAccessModifier.PublicInterface)]
public static ValueTask<Dictionary<long, AuthorityResponse<User>>> GetUsers(
IReadOnlyList<long> ids,
@@ -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;
@@ -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;
@@ -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;
@@ -18,9 +18,9 @@ namespace Tgstation.Server.Host.Models
Expression<Func<TInput, TOutput>> Expression { get; }
/// <summary>
/// <see cref="Expression{TDelegate}"/> for mapping <typeparamref name="TInput"/> into a <see cref="Projected{TQueried, TResult}"/>.
/// <see cref="Expression{TDelegate}"/> for mapping <typeparamref name="TInput"/> into a <see cref="ProjectedPair{TQueried, TResult}"/>.
/// </summary>
Expression<Func<TInput, Projected<TInput, TOutput>>> ProjectedExpression { get; }
Expression<Func<TInput, ProjectedPair<TInput, TOutput>>> ProjectedExpression { get; }
/// <summary>
/// The compiled <see cref="Expression"/>.
@@ -4,9 +4,16 @@ using Tgstation.Server.Host.GraphQL.Types;
namespace Tgstation.Server.Host.Models.Transformers
{
/// <summary>
/// <see cref="ITransformer{TInput, TOutput}"/> for <see cref="RightsHolder{TRight}"/>s.
/// </summary>
/// <typeparam name="TRight">The <see cref="Api.Rights.RightsType"/> of the <see cref="RightsHolder{TRight}"/>.</typeparam>
sealed class RightsHolderGraphQLTransformer<TRight> : TransformerBase<TRight?, RightsHolder<TRight>>
where TRight : struct, Enum
{
/// <summary>
/// Initializes a new instance of the <see cref="RightsHolderGraphQLTransformer{TRight}"/> class.
/// </summary>
public RightsHolderGraphQLTransformer()
: base(right => new RightsHolder<TRight>
{
@@ -17,21 +17,35 @@ namespace Tgstation.Server.Host.Models.Transformers
/// <summary>
/// <see langword="static"/> cache for <see cref="ProjectedExpression"/>.
/// </summary>
static Expression<Func<TInput, Projected<TInput, TOutput>>>? projectedExpression;
static Expression<Func<TInput, ProjectedPair<TInput, TOutput>>>? projectedExpression;
/// <inheritdoc />
public Expression<Func<TInput, TOutput>> Expression { get; }
/// <inheritdoc />
public Expression<Func<TInput, Projected<TInput, TOutput>>> ProjectedExpression { get; }
public Expression<Func<TInput, ProjectedPair<TInput, TOutput>>> ProjectedExpression { get; }
/// <inheritdoc />
public Func<TInput, TOutput> CompiledExpression { get; }
/// <summary>
/// Gets the <typeparamref name="T"/> that should be used when a database projected non-null DTO value is null in the expression.
/// </summary>
/// <typeparam name="T">The non-null <see cref="Type"/> a fallback is required for.</typeparam>
/// <returns>A fallback <typeparamref name="T"/> value.</returns>
protected static T NotNullFallback<T>()
where T : notnull
=> default!;
/// <summary>
/// Build an <see cref="Expression{TDelegate}"/> for <typeparamref name="TInput"/> to <typeparamref name="TOutput"/> when <typeparamref name="TInput"/> contains a <typeparamref name="TSubInput"/> with its own <typeparamref name="TTransformer"/>.
/// </summary>
/// <typeparam name="TSubInput">The field <see cref="Type"/> in <typeparamref name="TInput"/> that needs transforming.</typeparam>
/// <typeparam name="TSubOutput">The transformed <see cref="Type"/> of <typeparamref name="TSubInput"/>.</typeparam>
/// <typeparam name="TTransformer">The <see cref="ITransformer{TInput, TOutput}"/> for <typeparamref name="TSubInput"/>/<typeparamref name="TSubOutput"/>.</typeparam>
/// <param name="transformerExpression">The <see cref="Expression{TDelegate}"/> to take a <typeparamref name="TInput"/> and <typeparamref name="TSubOutput"/> and produce a <typeparamref name="TOutput"/>.</param>
/// <param name="subInputSelectionExpression">The <see cref="Expression{TDelegate}"/> to select <typeparamref name="TSubInput"/> from <typeparamref name="TInput"/>.</param>
/// <returns>An expression converting <typeparamref name="TInput"/> into <typeparamref name="TOutput"/> based on <paramref name="transformerExpression"/> with its other arguments generated from the transformation result of <paramref name="subInputSelectionExpression"/>.</returns>
protected static Expression<Func<TInput, TOutput>> BuildSubProjection<TSubInput, TSubOutput, TTransformer>(
Expression<Func<TInput, TSubOutput?, TOutput>> transformerExpression,
Expression<Func<TInput, TSubInput?>> subInputSelectionExpression)
@@ -60,6 +74,19 @@ namespace Tgstation.Server.Host.Models.Transformers
return global::System.Linq.Expressions.Expression.Lambda<Func<TInput, TOutput>>(outputExpression, primaryInput);
}
/// <summary>
/// Build an <see cref="Expression{TDelegate}"/> for <typeparamref name="TInput"/> to <typeparamref name="TOutput"/> when <typeparamref name="TInput"/> contains two sub-inputs with their own <see cref="ITransformer{TInput, TOutput}"/>s.
/// </summary>
/// <typeparam name="TSubInput1">The first field <see cref="Type"/> in <typeparamref name="TInput"/> that needs transforming.</typeparam>
/// <typeparam name="TSubInput2">The second field <see cref="Type"/> in <typeparamref name="TInput"/> that needs transforming.</typeparam>
/// <typeparam name="TSubOutput1">The first transformed <see cref="Type"/> of <typeparamref name="TSubInput1"/>.</typeparam>
/// <typeparam name="TSubOutput2">The second transformed <see cref="Type"/> of <typeparamref name="TSubInput2"/>.</typeparam>
/// <typeparam name="TTransformer1">The <see cref="ITransformer{TInput, TOutput}"/> for <typeparamref name="TSubInput1"/>/<typeparamref name="TSubOutput2"/>.</typeparam>
/// <typeparam name="TTransformer2">The <see cref="ITransformer{TInput, TOutput}"/> for <typeparamref name="TSubInput2"/>/<typeparamref name="TSubOutput2"/>.</typeparam>
/// <param name="transformerExpression">The <see cref="Expression{TDelegate}"/> to take a <typeparamref name="TInput"/>, <typeparamref name="TSubOutput1"/>, and <typeparamref name="TSubOutput1"/> and produce a <typeparamref name="TOutput"/>.</param>
/// <param name="subInput1SelectionExpression">The <see cref="Expression{TDelegate}"/> to select <typeparamref name="TSubInput1"/> from <typeparamref name="TInput"/>.</param>
/// <param name="subInput2SelectionExpression">The <see cref="Expression{TDelegate}"/> to select <typeparamref name="TSubInput2"/> from <typeparamref name="TInput"/>.</param>
/// <returns>An expression converting <typeparamref name="TInput"/> into <typeparamref name="TOutput"/> based on <paramref name="transformerExpression"/> with its other arguments generated from the transformation result of <paramref name="subInput1SelectionExpression"/> and <paramref name="subInput2SelectionExpression"/>.</returns>
protected static Expression<Func<TInput, TOutput>> BuildSubProjection<
TSubInput1,
TSubInput2,
@@ -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<string>(),
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,
},
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../../build/SrcCommon.props" />
<PropertyGroup>
@@ -6,7 +6,7 @@
<Version>$(TgsCoreVersion)</Version>
<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
<UseAppHost>false</UseAppHost>
<NoWarn>API1000;ASP0019</NoWarn>
<NoWarn>API1000;ASP0019;CA1000</NoWarn>
<NodeModulesFolder>ClientApp/node_modules</NodeModulesFolder>
<NpmInstallStampFile>ClientApp/node_modules/.install-stamp</NpmInstallStampFile>
<Nullable>enable</Nullable>