mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
Fix build warnings
This commit is contained in:
@@ -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"/>.
|
||||
|
||||
+7
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user