Adds pagination

- Add generic Paginated<TModel> for pagination responses.
- Added pagination helper to ApiController.
- Implemented pagination for all previously IEnumerable<T> response endpoints.
- Added IApiTransformable<TApiModel> for host models finally.
- Added PaginationSettings to client List endpoints.
- Use two deprecated ErrorCodes for page parameter validation.
- Re-enabled to OpenAPI root array response lint.
- Add some integration tests with users.
This commit is contained in:
Jordan Brown
2020-12-17 23:49:06 -05:00
parent 225e26e6b2
commit eb709d136b
54 changed files with 884 additions and 434 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"no_operation_id": "error",
"operation_id_case_convention": "off",
"no_summary": "error",
"no_array_responses": "off",
"no_array_responses": "error",
"parameter_order": "error",
"undefined_tag": "off",
"unused_tag": "error",
+6 -8
View File
@@ -250,18 +250,16 @@ namespace Tgstation.Server.Api.Models
RepoWhitespaceCommitterEmail,
/// <summary>
/// Deprecated.
/// A paginated request asked for too large a page.
/// </summary>
[Description("Deprecated error code.")]
[Obsolete("With API v7 ", true)]
DreamDaemonDuplicatePorts,
[Description("Requested pageSize is too large!")]
ApiPageTooLarge,
/// <summary>
/// Deprecated.
/// A paginated request asked for page 0.
/// </summary>
[Description("Deprecated error code.")]
[Obsolete("With DMAPI-5.0.0, ultrasafe security is now supported.", true)]
InvalidSecurityLevel,
[Description("Cannot request page or pageSize <= 0.")]
ApiInvalidPageOrPageSize,
/// <summary>
/// A requested <see cref="ChatChannel"/>'s data does not match with its <see cref="Internal.ChatBot.Provider"/>.
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Api.Models
{
/// <summary>
/// Represents a paginated set of models.
/// </summary>
/// <typeparam name="TModel">The <see cref="System.Type"/> of the returned model.</typeparam>
public sealed class Paginated<TModel>
{
/// <summary>
/// The <see cref="ICollection{T}"/> of the returned <typeparamref name="TModel"/>s.
/// </summary>
[Required]
public ICollection<TModel>? Content { get; set; }
/// <summary>
/// The total number of pages in the query.
/// </summary>
public int TotalPages { get; set; }
/// <summary>
/// The current size of pages in the query.
/// </summary>
public int PageSize { get; set; }
}
}
@@ -10,45 +10,40 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class AdministrationClient : IAdministrationClient
sealed class AdministrationClient : PaginatedClient, IAdministrationClient
{
/// <summary>
/// The <see cref="apiClient"/> for the <see cref="AdministrationClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// Construct an <see cref="AdministrationClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="apiClient">The <see cref="IApiClient"/> for the <see cref="PaginatedClient"/>.</param>
public AdministrationClient(IApiClient apiClient)
{
this.apiClient = apiClient;
}
: base(apiClient)
{ }
/// <inheritdoc />
public Task<Administration> Read(CancellationToken cancellationToken) => apiClient.Read<Administration>(Routes.Administration, cancellationToken);
public Task<Administration> Read(CancellationToken cancellationToken) => ApiClient.Read<Administration>(Routes.Administration, cancellationToken);
/// <inheritdoc />
public Task Update(Administration administration, CancellationToken cancellationToken) => apiClient.Update(Routes.Administration, administration ?? throw new ArgumentNullException(nameof(administration)), cancellationToken);
public Task Update(Administration administration, CancellationToken cancellationToken) => ApiClient.Update(Routes.Administration, administration ?? throw new ArgumentNullException(nameof(administration)), cancellationToken);
/// <inheritdoc />
public Task Restart(CancellationToken cancellationToken) => apiClient.Delete(Routes.Administration, cancellationToken);
public Task Restart(CancellationToken cancellationToken) => ApiClient.Delete(Routes.Administration, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<LogFile>> ListLogs(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<LogFile>>(Routes.Logs, cancellationToken);
public Task<IReadOnlyList<LogFile>> ListLogs(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
=> ReadPaged<LogFile>(paginationSettings, Routes.Logs, null, cancellationToken);
/// <inheritdoc />
public async Task<Tuple<LogFile, Stream>> GetLog(LogFile logFile, CancellationToken cancellationToken)
{
var resultFile = await apiClient.Read<LogFile>(
var resultFile = await ApiClient.Read<LogFile>(
Routes.Logs + Routes.SanitizeGetPath(
HttpUtility.UrlEncode(
logFile?.Name ?? throw new ArgumentNullException(nameof(logFile)))),
cancellationToken)
.ConfigureAwait(false);
var stream = await apiClient.Download(resultFile, cancellationToken).ConfigureAwait(false);
var stream = await ApiClient.Download(resultFile, cancellationToken).ConfigureAwait(false);
try
{
return Tuple.Create(resultFile, stream);
@@ -9,13 +9,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class ByondClient : IByondClient
sealed class ByondClient : PaginatedClient, IByondClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="ByondClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="ByondClient"/>
/// </summary>
@@ -24,24 +19,25 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// Construct a <see cref="ByondClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="apiClient">The <see cref="IApiClient"/> for the <see cref="PaginatedClient"/>.</param>
/// <param name="instance">The value of <see cref="Instance"/></param>
public ByondClient(IApiClient apiClient, Instance instance)
: base(apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task<Byond> ActiveVersion(CancellationToken cancellationToken) => apiClient.Read<Byond>(Routes.Byond, instance.Id, cancellationToken);
public Task<Byond> ActiveVersion(CancellationToken cancellationToken) => ApiClient.Read<Byond>(Routes.Byond, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<Byond>> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Byond>>(Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken);
public Task<IReadOnlyList<Byond>> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
=> ReadPaged<Byond>(paginationSettings, Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken);
/// <inheritdoc />
public async Task<Byond> SetActiveVersion(Byond byond, Stream zipFileStream, CancellationToken cancellationToken)
{
var result = await apiClient.Update<Byond, Byond>(
var result = await ApiClient.Update<Byond, Byond>(
Routes.Byond,
byond ?? throw new ArgumentNullException(nameof(byond)),
instance.Id,
@@ -49,7 +45,7 @@ namespace Tgstation.Server.Client.Components
.ConfigureAwait(false);
if (byond.UploadCustomZip == true)
await apiClient.Upload(result, zipFileStream, cancellationToken).ConfigureAwait(false);
await ApiClient.Upload(result, zipFileStream, cancellationToken).ConfigureAwait(false);
return result;
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class ChatBotsClient : IChatBotsClient
sealed class ChatBotsClient : PaginatedClient, IChatBotsClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="ChatBotsClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="ChatBotsClient"/>
/// </summary>
@@ -23,27 +18,28 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// Construct a <see cref="ChatBotsClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="apiClient">The <see cref="IApiClient"/> for the <see cref="PaginatedClient"/>.</param>
/// <param name="instance">The value of <see cref="instance"/></param>
public ChatBotsClient(IApiClient apiClient, Instance instance)
: base(apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task<ChatBot> Create(ChatBot settings, CancellationToken cancellationToken) => apiClient.Create<ChatBot, ChatBot>(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
public Task<ChatBot> Create(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Create<ChatBot, ChatBot>(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
/// <inheritdoc />
public Task Delete(ChatBot settings, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Chat, settings?.Id ?? throw new ArgumentNullException(nameof(settings))), instance.Id, cancellationToken);
public Task Delete(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Delete(Routes.SetID(Routes.Chat, settings?.Id ?? throw new ArgumentNullException(nameof(settings))), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<ChatBot>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ChatBot>>(Routes.ListRoute(Routes.Chat), instance.Id, cancellationToken);
public Task<IReadOnlyList<ChatBot>> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
=> ReadPaged<ChatBot>(paginationSettings, Routes.ListRoute(Routes.Chat), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<ChatBot> Update(ChatBot settings, CancellationToken cancellationToken) => apiClient.Update<ChatBot, ChatBot>(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
public Task<ChatBot> Update(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Update<ChatBot, ChatBot>(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<ChatBot> GetId(ChatBot settings, CancellationToken cancellationToken) => apiClient.Read<ChatBot>(Routes.SetID(Routes.Chat, (settings ?? throw new ArgumentNullException(nameof(settings))).Id), instance.Id, cancellationToken);
public Task<ChatBot> GetId(ChatBot settings, CancellationToken cancellationToken) => ApiClient.Read<ChatBot>(Routes.SetID(Routes.Chat, (settings ?? throw new ArgumentNullException(nameof(settings))).Id), instance.Id, cancellationToken);
}
}
}
@@ -9,13 +9,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class ConfigurationClient : IConfigurationClient
sealed class ConfigurationClient : PaginatedClient, IConfigurationClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="ConfigurationClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="ConfigurationClient"/>
/// </summary>
@@ -24,34 +19,42 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// Construct a <see cref="ConfigurationClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="apiClient">The <see cref="IApiClient"/> for the <see cref="PaginatedClient"/>.</param>
/// <param name="instance">The value of <see cref="instance"/></param>
public ConfigurationClient(IApiClient apiClient, Instance instance)
: base(apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Delete(Routes.Configuration, directory, instance.Id, cancellationToken);
public Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => ApiClient.Delete(Routes.Configuration, directory, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<ConfigurationFile> CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Create<ConfigurationFile, ConfigurationFile>(Routes.Configuration, directory, instance.Id, cancellationToken);
public Task<ConfigurationFile> CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => ApiClient.Create<ConfigurationFile, ConfigurationFile>(Routes.Configuration, directory, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<ConfigurationFile>> List(string directory, CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ConfigurationFile>>(Routes.ListRoute(Routes.Configuration) + Routes.SanitizeGetPath(directory), instance.Id, cancellationToken);
public Task<IReadOnlyList<ConfigurationFile>> List(
PaginationSettings? paginationSettings,
string directory,
CancellationToken cancellationToken)
=> ReadPaged<ConfigurationFile>(
paginationSettings,
Routes.ListRoute(Routes.Configuration) + Routes.SanitizeGetPath(directory),
instance.Id,
cancellationToken);
/// <inheritdoc />
public async Task<Tuple<ConfigurationFile, Stream>> Read(ConfigurationFile file, CancellationToken cancellationToken)
{
if (file == null)
throw new ArgumentNullException(nameof(file));
var configFile = await apiClient.Read<ConfigurationFile>(
var configFile = await ApiClient.Read<ConfigurationFile>(
Routes.ConfigurationFile + Routes.SanitizeGetPath(file.Path ?? throw new ArgumentException("file.Path should not be null!", nameof(file))),
instance.Id,
cancellationToken)
.ConfigureAwait(false);
var downloadStream = await apiClient.Download(configFile, cancellationToken).ConfigureAwait(false);
var downloadStream = await ApiClient.Download(configFile, cancellationToken).ConfigureAwait(false);
try
{
return Tuple.Create(configFile, downloadStream);
@@ -75,7 +78,7 @@ namespace Tgstation.Server.Client.Components
using (memoryStream)
{
var configFileTask = apiClient.Update<ConfigurationFile, ConfigurationFile>(
var configFileTask = ApiClient.Update<ConfigurationFile, ConfigurationFile>(
Routes.Configuration,
file ?? throw new ArgumentNullException(nameof(file)),
instance.Id,
@@ -88,7 +91,7 @@ namespace Tgstation.Server.Client.Components
var streamUsed = memoryStream ?? uploadStream;
streamUsed?.Seek(initialStreamPosition, SeekOrigin.Begin);
await apiClient.Upload(configFile, streamUsed, cancellationToken).ConfigureAwait(false);
await ApiClient.Upload(configFile, streamUsed, cancellationToken).ConfigureAwait(false);
return configFile;
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class DreamMakerClient : IDreamMakerClient
sealed class DreamMakerClient : PaginatedClient, IDreamMakerClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="DreamMakerClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="DreamMakerClient"/>
/// </summary>
@@ -23,27 +18,28 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// Construct a <see cref="DreamMakerClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="apiClient">The <see cref="IApiClient"/> for the <see cref="PaginatedClient"/>.</param>
/// <param name="instance">The value of <see cref="Instance"/></param>
public DreamMakerClient(IApiClient apiClient, Instance instance)
: base(apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task<Job> Compile(CancellationToken cancellationToken) => apiClient.Create<Job>(Routes.DreamMaker, instance.Id, cancellationToken);
public Task<Job> Compile(CancellationToken cancellationToken) => ApiClient.Create<Job>(Routes.DreamMaker, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<CompileJob> GetCompileJob(EntityId compileJob, CancellationToken cancellationToken) => apiClient.Read<CompileJob>(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken);
public Task<CompileJob> GetCompileJob(EntityId compileJob, CancellationToken cancellationToken) => ApiClient.Read<CompileJob>(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<EntityId>> GetJobIds(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<EntityId>>(Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken);
public Task<IReadOnlyList<EntityId>> ListCompileJobs(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
=> ReadPaged<EntityId>(paginationSettings, Routes.ListRoute(Routes.DreamMaker), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<DreamMaker> Read(CancellationToken cancellationToken) => apiClient.Read<DreamMaker>(Routes.DreamMaker, instance.Id, cancellationToken);
public Task<DreamMaker> Read(CancellationToken cancellationToken) => ApiClient.Read<DreamMaker>(Routes.DreamMaker, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<DreamMaker> Update(DreamMaker dreamMaker, CancellationToken cancellationToken) => apiClient.Update<DreamMaker, DreamMaker>(Routes.DreamMaker, dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker)), instance.Id, cancellationToken);
public Task<DreamMaker> Update(DreamMaker dreamMaker, CancellationToken cancellationToken) => ApiClient.Update<DreamMaker, DreamMaker>(Routes.DreamMaker, dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker)), instance.Id, cancellationToken);
}
}
}
@@ -21,9 +21,10 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// Get all installed <see cref="Byond"/> <see cref="System.Version"/>s
/// </summary>
/// <param name="paginationSettings">The optional <see cref="PaginationSettings"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in an <see cref="IReadOnlyList{T}"/> of installed <see cref="Byond"/> <see cref="System.Version"/>s</returns>
Task<IReadOnlyList<Byond>> InstalledVersions(CancellationToken cancellationToken);
Task<IReadOnlyList<Byond>> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
/// <summary>
/// Updates the <see cref="Byond"/> information
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -13,9 +13,10 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// List the <see cref="ChatBot"/>s
/// </summary>
/// <param name="paginationSettings">The optional <see cref="PaginationSettings"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of the <see cref="ChatBot"/> of the server</returns>
Task<IReadOnlyList<ChatBot>> List(CancellationToken cancellationToken);
Task<IReadOnlyList<ChatBot>> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
/// <summary>
/// Create a <see cref="ChatBot"/>
@@ -15,10 +15,14 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// List configuration files
/// </summary>
/// <param name="paginationSettings">The optional <see cref="PaginationSettings"/> for the operation.</param>
/// <param name="directory">The path to the directory to list files in</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="IReadOnlyList{T}"/> of <see cref="ConfigurationFile"/>s in the <paramref name="directory"/></returns>
Task<IReadOnlyList<ConfigurationFile>> List(string directory, CancellationToken cancellationToken);
Task<IReadOnlyList<ConfigurationFile>> List(
PaginationSettings? paginationSettings,
string directory,
CancellationToken cancellationToken);
/// <summary>
/// Read a <see cref="ConfigurationFile"/> file
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -33,11 +33,12 @@ namespace Tgstation.Server.Client.Components
Task<Job> Compile(CancellationToken cancellationToken);
/// <summary>
/// Gets the <see cref="EntityId"/>s of all <see cref="CompileJob"/>s for the instance
/// Gets the <see cref="CompileJob"/>s for the instance
/// </summary>
/// <param name="paginationSettings">The optional <see cref="PaginationSettings"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of <see cref="CompileJob"/> <see cref="EntityId"/>s.</returns>
Task<IReadOnlyList<EntityId>> GetJobIds(CancellationToken cancellationToken);
Task<IReadOnlyList<EntityId>> ListCompileJobs(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
/// <summary>
/// Get a <paramref name="compileJob"/>
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -28,9 +28,10 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// Get the <see cref="InstanceUser"/>s in the <see cref="Instance"/>
/// </summary>
/// <param name="paginationSettings">The optional <see cref="PaginationSettings"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of <see cref="InstanceUser"/>s in the instance</returns>
Task<IReadOnlyList<InstanceUser>> List(CancellationToken cancellationToken);
Task<IReadOnlyList<InstanceUser>> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
/// <summary>
/// Update a <paramref name="instanceUser"/>
@@ -56,4 +57,4 @@ namespace Tgstation.Server.Client.Components
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken);
}
}
}
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -11,18 +11,20 @@ namespace Tgstation.Server.Client.Components
public interface IJobsClient
{
/// <summary>
/// List the <see cref="Job"/> <see cref="EntityId"/>s in the <see cref="Instance"/>
/// List the <see cref="Job"/>s in the <see cref="Instance"/>
/// </summary>
/// <param name="paginationSettings">The optional <see cref="PaginationSettings"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of the <see cref="Job"/> <see cref="EntityId"/>s in the <see cref="Instance"/></returns>
Task<IReadOnlyList<EntityId>> List(CancellationToken cancellationToken);
Task<IReadOnlyList<Job>> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
/// <summary>
/// List the active <see cref="Job"/>s in the <see cref="Instance"/>
/// </summary>
/// <param name="paginationSettings">The optional <see cref="PaginationSettings"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of the active <see cref="Job"/>s in the <see cref="Instance"/></returns>
Task<IReadOnlyList<Job>> ListActive(CancellationToken cancellationToken);
Task<IReadOnlyList<Job>> ListActive(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
/// <summary>
/// Get a <paramref name="job"/>
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class InstanceUserClient : IInstanceUserClient
sealed class InstanceUserClient : PaginatedClient, IInstanceUserClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="InstanceUserClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="InstanceUserClient"/>
/// </summary>
@@ -23,19 +18,19 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// Construct an <see cref="InstanceUserClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="apiClient">The <see cref="IApiClient"/> for the <see cref="PaginatedClient"/>.</param>
/// <param name="instance">The value of <see cref="instance"/></param>
public InstanceUserClient(IApiClient apiClient, Instance instance)
: base(apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task<InstanceUser> Create(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Create<InstanceUser, InstanceUser>(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
public Task<InstanceUser> Create(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Create<InstanceUser, InstanceUser>(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
/// <inheritdoc />
public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete(
public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Delete(
Routes.SetID(
Routes.InstanceUser,
instanceUser.UserId),
@@ -43,15 +38,16 @@ namespace Tgstation.Server.Client.Components
cancellationToken);
/// <inheritdoc />
public Task<InstanceUser> Read(CancellationToken cancellationToken) => apiClient.Read<InstanceUser>(Routes.InstanceUser, instance.Id, cancellationToken);
public Task<InstanceUser> Read(CancellationToken cancellationToken) => ApiClient.Read<InstanceUser>(Routes.InstanceUser, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<InstanceUser> Update(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Update<InstanceUser, InstanceUser>(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
public Task<InstanceUser> Update(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Update<InstanceUser, InstanceUser>(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<InstanceUser>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<InstanceUser>>(Routes.ListRoute(Routes.InstanceUser), instance.Id, cancellationToken);
public Task<IReadOnlyList<InstanceUser>> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
=> ReadPaged<InstanceUser>(paginationSettings, Routes.ListRoute(Routes.InstanceUser), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<InstanceUser> GetId(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Read<InstanceUser>(Routes.SetID(Routes.InstanceUser, instanceUser?.UserId ?? throw new ArgumentNullException(nameof(instanceUser))), instance.Id, cancellationToken);
public Task<InstanceUser> GetId(InstanceUser instanceUser, CancellationToken cancellationToken) => ApiClient.Read<InstanceUser>(Routes.SetID(Routes.InstanceUser, instanceUser?.UserId ?? throw new ArgumentNullException(nameof(instanceUser))), instance.Id, cancellationToken);
}
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,13 +8,8 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client.Components
{
/// <inheritdoc />
sealed class JobsClient : IJobsClient
sealed class JobsClient : PaginatedClient, IJobsClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="JobsClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="JobsClient"/>
/// </summary>
@@ -23,24 +18,26 @@ namespace Tgstation.Server.Client.Components
/// <summary>
/// Construct a <see cref="JobsClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="apiClient">The <see cref="IApiClient"/> for the <see cref="PaginatedClient"/>.</param>
/// <param name="instance">The value of <see cref="Instance"/></param>
public JobsClient(IApiClient apiClient, Instance instance)
: base(apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public Task Cancel(Job job, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
public Task Cancel(Job job, CancellationToken cancellationToken) => ApiClient.Delete(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<EntityId>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<EntityId>>(Routes.ListRoute(Routes.Jobs), instance.Id, cancellationToken);
public Task<IReadOnlyList<Job>> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
=> ReadPaged<Job>(paginationSettings, Routes.ListRoute(Routes.Jobs), instance.Id, cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<Job>> ListActive(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Job>>(Routes.Jobs, instance.Id, cancellationToken);
public Task<IReadOnlyList<Job>> ListActive(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
=> ReadPaged<Job>(paginationSettings, Routes.Jobs, instance.Id, cancellationToken);
/// <inheritdoc />
public Task<Job> GetId(EntityId job, CancellationToken cancellationToken) => apiClient.Read<Job>(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
public Task<Job> GetId(EntityId job, CancellationToken cancellationToken) => ApiClient.Read<Job>(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
}
}
}
@@ -37,9 +37,10 @@ namespace Tgstation.Server.Client
/// <summary>
/// Lists the log files available for download.
/// </summary>
/// <param name="paginationSettings">The optional <see cref="PaginationSettings"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in an <see cref="IReadOnlyList{T}"/> of <see cref="LogFile"/> metadata.</returns>
Task<IReadOnlyList<LogFile>> ListLogs(CancellationToken cancellationToken);
Task<IReadOnlyList<LogFile>> ListLogs(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
/// <summary>
/// Download a given <paramref name="logFile"/>.
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -14,9 +14,10 @@ namespace Tgstation.Server.Client
/// <summary>
/// Get all <see cref="IInstanceClient"/>s for <see cref="Instance"/>s the user can view
/// </summary>
/// <param name="paginationSettings">The optional <see cref="PaginationSettings"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of all <see cref="Instance"/>s the user can view</returns>
Task<IReadOnlyList<Instance>> List(CancellationToken cancellationToken);
Task<IReadOnlyList<Instance>> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
/// <summary>
/// Create or attach an <paramref name="instance"/>
+4 -3
View File
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -28,9 +28,10 @@ namespace Tgstation.Server.Client
/// <summary>
/// List all <see cref="User"/>s
/// </summary>
/// <param name="paginationSettings">The optional <see cref="PaginationSettings"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="IReadOnlyList{T}"/> of all <see cref="User"/>s</returns>
Task<IReadOnlyList<User>> List(CancellationToken cancellationToken);
Task<IReadOnlyList<User>> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
/// <summary>
/// Create a new <paramref name="user"/>
@@ -48,4 +49,4 @@ namespace Tgstation.Server.Client
/// <returns>The updated <see cref="User"/></returns>
Task<User> Update(UserUpdate user, CancellationToken cancellationToken);
}
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -9,41 +9,36 @@ using Tgstation.Server.Client.Components;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class InstanceManagerClient : IInstanceManagerClient
sealed class InstanceManagerClient : PaginatedClient, IInstanceManagerClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="InstanceManagerClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// Construct an <see cref="InstanceManagerClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="apiClient">The <see cref="IApiClient"/> for the <see cref="PaginatedClient"/>.</param>
public InstanceManagerClient(IApiClient apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
}
: base(apiClient)
{ }
/// <inheritdoc />
public Task<Instance> CreateOrAttach(Instance instance, CancellationToken cancellationToken) => apiClient.Create<Instance, Instance>(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
public Task<Instance> CreateOrAttach(Instance instance, CancellationToken cancellationToken) => ApiClient.Create<Instance, Instance>(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
/// <inheritdoc />
public Task Detach(Instance instance, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
public Task Detach(Instance instance, CancellationToken cancellationToken) => ApiClient.Delete(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<Instance>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Instance>>(Routes.ListRoute(Routes.InstanceManager), cancellationToken);
public Task<IReadOnlyList<Instance>> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
=> ReadPaged<Instance>(paginationSettings, Routes.ListRoute(Routes.InstanceManager), null, cancellationToken);
/// <inheritdoc />
public Task<Instance> Update(Instance instance, CancellationToken cancellationToken) => apiClient.Update<Instance, Instance>(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
public Task<Instance> Update(Instance instance, CancellationToken cancellationToken) => ApiClient.Update<Instance, Instance>(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
/// <inheritdoc />
public Task<Instance> GetId(Instance instance, CancellationToken cancellationToken) => apiClient.Read<Instance>(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
public Task<Instance> GetId(Instance instance, CancellationToken cancellationToken) => ApiClient.Read<Instance>(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
/// <inheritdoc />
public Task GrantPermissions(Instance instance, CancellationToken cancellationToken) => apiClient.Patch(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
public Task GrantPermissions(Instance instance, CancellationToken cancellationToken) => ApiClient.Patch(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
/// <inheritdoc />
public IInstanceClient CreateClient(Instance instance) => new InstanceClient(apiClient, instance);
public IInstanceClient CreateClient(Instance instance) => new InstanceClient(ApiClient, instance);
}
}
}
@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Client that deals with getting paginated results.
/// </summary>
abstract class PaginatedClient
{
/// <summary>
/// The <see cref="IApiClient"/> for the <see cref="PaginatedClient"/>
/// </summary>
protected IApiClient ApiClient { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PaginatedClient"/> <see langword="class"/>.
/// </summary>
/// <param name="apiClient">The value of <see cref="ApiClient"/>.</param>
public PaginatedClient(IApiClient apiClient)
{
ApiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
}
/// <summary>
/// Reads a given <paramref name="route"/> with paged results.
/// </summary>
/// <typeparam name="TModel">The <see cref="Type"/> of result model.</typeparam>
/// <param name="paginationSettings">The <see cref="PaginationSettings"/> if any.</param>
/// <param name="route">The route.</param>
/// <param name="instanceId">The optional <see cref="Instance"/> <see cref="EntityId.Id"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in an <see cref="IReadOnlyList{T}"/> of the paginated <typeparamref name="TModel"/>s.</returns>
protected async Task<IReadOnlyList<TModel>> ReadPaged<TModel>(
PaginationSettings? paginationSettings,
string route,
long? instanceId,
CancellationToken cancellationToken)
{
if (route == null)
throw new ArgumentNullException(nameof(route));
var routeFormatter = $"{route}?page={{0}}";
var currentPage = 1;
if (paginationSettings != null)
{
if (paginationSettings.RetrieveCount == 0)
return new List<TModel>(); // that was easy
else if (paginationSettings.RetrieveCount < 0)
throw new ArgumentOutOfRangeException("RetrieveCount cannot be less than 0!", nameof(paginationSettings));
int? pageSize = null;
if (paginationSettings.PageSize.HasValue)
{
// pagesize validates itself on first request
pageSize = paginationSettings.PageSize.Value;
routeFormatter += $"&pageSize={pageSize}";
}
if (paginationSettings.Offset.HasValue)
{
if(paginationSettings.Offset.Value < 0)
throw new ArgumentOutOfRangeException("Offset cannot be less than 0!", nameof(paginationSettings));
pageSize ??= paginationSettings.Offset.Value;
currentPage = (paginationSettings.Offset.Value / pageSize.Value) + 1;
}
}
Task<Paginated<TModel>> GetPage() => instanceId.HasValue
? ApiClient.Read<Paginated<TModel>>(
String.Format(routeFormatter, currentPage),
instanceId.Value,
cancellationToken)
: ApiClient.Read<Paginated<TModel>>(
String.Format(routeFormatter, currentPage),
cancellationToken);
var firstPage = await GetPage().ConfigureAwait(false);
var totalAvailable = firstPage.TotalPages * firstPage.PageSize;
var maximumItems = paginationSettings?.RetrieveCount.HasValue == true
? Math.Min(paginationSettings.RetrieveCount!.Value, totalAvailable)
: totalAvailable;
var results = new List<TModel>(maximumItems);
var currentResults = firstPage;
do
{
// check if first page
if(currentPage > 1)
currentResults = await GetPage().ConfigureAwait(false);
if (currentResults.Content == null)
throw new ApiConflictException("Paginated results missing content!");
IEnumerable<TModel> rangeToAdd = currentResults.Content;
if (paginationSettings?.Offset.HasValue == true)
{
rangeToAdd = rangeToAdd
.Skip(paginationSettings.Offset!.Value % currentResults.PageSize);
}
var itemsAvailableInPage = rangeToAdd.Count();
var itemsStillRequired = maximumItems - results.Count;
if (itemsAvailableInPage > itemsStillRequired)
rangeToAdd = rangeToAdd
.Take(itemsStillRequired);
results.AddRange(rangeToAdd);
++currentPage;
}
while (results.Count < maximumItems && currentPage <= currentResults.TotalPages);
return results;
}
}
}
@@ -0,0 +1,24 @@
namespace Tgstation.Server.Client
{
/// <summary>
/// Settings for a paginated request.
/// </summary>
public sealed class PaginationSettings
{
/// <summary>
/// The size of a page. Defaults to server settings.
/// </summary>
public int? PageSize { get; set; }
/// <summary>
/// The offset to take from. Default 0.
/// </summary>
public int? Offset { get; set; }
/// <summary>
/// The maximum amount of items to retrieve. Default everything.
/// </summary>
/// <remarks>Results will be truncated if overflow occurs due to <see cref="PageSize"/>.</remarks>
public int? RetrieveCount { get; set; }
}
}
+11 -15
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,35 +8,31 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <inheritdoc />
sealed class UsersClient : IUsersClient
sealed class UsersClient : PaginatedClient, IUsersClient
{
/// <summary>
/// The <see cref="apiClient"/> for the <see cref="UsersClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// Construct an <see cref="UsersClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="apiClient">The <see cref="IApiClient"/> for the <see cref="PaginatedClient"/>.</param>
public UsersClient(IApiClient apiClient)
: base(apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
}
/// <inheritdoc />
public Task<User> Create(UserUpdate user, CancellationToken cancellationToken) => apiClient.Create<UserUpdate, User>(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken);
public Task<User> Create(UserUpdate user, CancellationToken cancellationToken) => ApiClient.Create<UserUpdate, User>(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken);
/// <inheritdoc />
public Task<User> GetId(Api.Models.Internal.User user, CancellationToken cancellationToken) => apiClient.Read<User>(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken);
public Task<User> GetId(Api.Models.Internal.User user, CancellationToken cancellationToken) => ApiClient.Read<User>(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken);
/// <inheritdoc />
public Task<IReadOnlyList<User>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<User>>(Routes.ListRoute(Routes.User), cancellationToken);
public Task<IReadOnlyList<User>> List(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
=> ReadPaged<User>(paginationSettings, Routes.ListRoute(Routes.User), null, cancellationToken);
/// <inheritdoc />
public Task<User> Read(CancellationToken cancellationToken) => apiClient.Read<User>(Routes.User, cancellationToken);
public Task<User> Read(CancellationToken cancellationToken) => ApiClient.Read<User>(Routes.User, cancellationToken);
/// <inheritdoc />
public Task<User> Update(UserUpdate user, CancellationToken cancellationToken) => apiClient.Update<UserUpdate, User>(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken);
public Task<User> Update(UserUpdate user, CancellationToken cancellationToken) => ApiClient.Update<UserUpdate, User>(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken);
}
}
}
@@ -313,48 +313,59 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// List <see cref="LogFile"/>s present.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request.</returns>
/// <response code="200">Listed logs successfully.</response>
/// <response code="409">An IO error occurred while listing.</response>
[HttpGet(Routes.Logs)]
[TgsAuthorize(AdministrationRights.DownloadLogs)]
[ProducesResponseType(typeof(List<LogFile>), 200)]
[ProducesResponseType(typeof(Paginated<LogFile>), 200)]
[ProducesResponseType(typeof(ErrorMessage), 409)]
public async Task<IActionResult> ListLogs(CancellationToken cancellationToken)
{
var path = fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier);
try
{
var files = await ioManager.GetFiles(path, cancellationToken).ConfigureAwait(false);
var tasks = files.Select(
async file => new LogFile
{
Name = ioManager.GetFileName(file),
LastModified = await ioManager.GetLastModified(
ioManager.ConcatPath(path, file),
cancellationToken)
.ConfigureAwait(false)
})
.ToList();
await Task.WhenAll(tasks).ConfigureAwait(false);
var result = tasks
.Select(x => x.Result)
.OrderByDescending(x => x.Name)
.ToList();
return Ok(result);
}
catch (IOException ex)
{
return Conflict(new ErrorMessage(ErrorCode.IOError)
public Task<IActionResult> ListLogs([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
=> Paginated(
async () =>
{
AdditionalData = ex.ToString()
});
}
}
var path = fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier);
try
{
var files = await ioManager.GetFiles(path, cancellationToken).ConfigureAwait(false);
var tasks = files.Select(
async file => new LogFile
{
Name = ioManager.GetFileName(file),
LastModified = await ioManager
.GetLastModified(
ioManager.ConcatPath(path, file),
cancellationToken)
.ConfigureAwait(false)
})
.ToList();
await Task.WhenAll(tasks).ConfigureAwait(false);
var result = tasks
.Select(x => x.Result)
.OrderByDescending(x => x.Name)
.ToList();
return new PaginatableResult<LogFile>(
result.AsQueryable());
}
catch (IOException ex)
{
return new PaginatableResult<LogFile>(
Conflict(new ErrorMessage(ErrorCode.IOError)
{
AdditionalData = ex.ToString()
}));
}
},
null,
page,
pageSize,
cancellationToken);
/// <summary>
/// Download a <see cref="LogFile"/>.
@@ -1,11 +1,14 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Octokit;
using Serilog.Context;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
@@ -15,6 +18,7 @@ using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers
@@ -26,6 +30,16 @@ namespace Tgstation.Server.Host.Controllers
[ApiController]
public abstract class ApiController : Controller
{
/// <summary>
/// Default size of <see cref="Paginated{TModel}"/> results.
/// </summary>
private const ushort DefaultPageSize = 10;
/// <summary>
/// Maximum size of <see cref="Paginated{TModel}"/> results.
/// </summary>
private const ushort MaximumPageSize = 100;
/// <summary>
/// The <see cref="ApiHeaders"/> for the operation
/// </summary>
@@ -275,6 +289,132 @@ namespace Tgstation.Server.Host.Controllers
await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
}
}
#pragma warning restore CA1506
#pragma warning restore CA1506
/// <summary>
/// Generates a paginated response.
/// </summary>
/// <typeparam name="TModel">The <see cref="Type"/> of model being generated and returned.</typeparam>
/// <param name="queryGenerator">A <see cref="Func{TResult}"/> resulting in a <see cref="Task{TResult}"/> resulting in the generated <see cref="PaginatableResult{TModel}"/>.</param>
/// <param name="resultTransformer">An <see cref="Action{T1}"/> to transform the <typeparamref name="TModel"/>s after being queried.</param>
/// <param name="pageQuery">The requested page from the query.</param>
/// <param name="pageSizeQuery">The requested page size from the query.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
protected Task<IActionResult> Paginated<TModel>(
Func<Task<PaginatableResult<TModel>>> queryGenerator,
Action<TModel> resultTransformer,
int? pageQuery,
int? pageSizeQuery,
CancellationToken cancellationToken) => PaginatedImpl<TModel, TModel>(
queryGenerator,
resultTransformer,
pageQuery,
pageSizeQuery,
cancellationToken);
/// <summary>
/// Generates a paginated response.
/// </summary>
/// <typeparam name="TModel">The <see cref="Type"/> of model being generated.</typeparam>
/// <typeparam name="TApiModel">The <see cref="Type"/> of model being returned.</typeparam>
/// <param name="queryGenerator">A <see cref="Func{TResult}"/> resulting in a <see cref="Task{TResult}"/> resulting in the generated <see cref="PaginatableResult{TModel}"/>.</param>
/// <param name="resultTransformer">An <see cref="Action{T1}"/> to transform the <typeparamref name="TModel"/>s after being queried.</param>
/// <param name="pageQuery">The requested page from the query.</param>
/// <param name="pageSizeQuery">The requested page size from the query.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
protected Task<IActionResult> Paginated<TModel, TApiModel>(
Func<Task<PaginatableResult<TModel>>> queryGenerator,
Action<TModel> resultTransformer,
int? pageQuery,
int? pageSizeQuery,
CancellationToken cancellationToken)
where TModel : IApiTransformable<TApiModel>
=> PaginatedImpl<TModel, TApiModel>(
queryGenerator,
resultTransformer,
pageQuery,
pageSizeQuery,
cancellationToken);
/// <summary>
/// Generates a paginated response.
/// </summary>
/// <typeparam name="TModel">The <see cref="Type"/> of model being generated. If different from <typeparamref name="TResultModel"/>, must implement <see cref="IApiTransformable{TApiModel}"/> for <typeparamref name="TResultModel"/>.</typeparam>
/// <typeparam name="TResultModel">The <see cref="Type"/> of model being returned.</typeparam>
/// <param name="queryGenerator">A <see cref="Func{TResult}"/> resulting in a <see cref="Task{TResult}"/> resulting in the generated <see cref="PaginatableResult{TModel}"/>.</param>
/// <param name="resultTransformer">An <see cref="Action{T1}"/> to transform the <typeparamref name="TModel"/>s after being queried.</param>
/// <param name="pageQuery">The requested page from the query.</param>
/// <param name="pageSizeQuery">The requested page size from the query.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
async Task<IActionResult> PaginatedImpl<TModel, TResultModel>(
Func<Task<PaginatableResult<TModel>>> queryGenerator,
Action<TModel> resultTransformer,
int? pageQuery,
int? pageSizeQuery,
CancellationToken cancellationToken)
{
if (queryGenerator == null)
throw new ArgumentNullException(nameof(queryGenerator));
if (pageQuery <= 0 || pageSizeQuery <= 0)
return BadRequest(new ErrorMessage(ErrorCode.ApiInvalidPageOrPageSize));
var pageSize = pageSizeQuery ?? DefaultPageSize;
if (pageSize > MaximumPageSize)
return BadRequest(new ErrorMessage(ErrorCode.ApiPageTooLarge)
{
AdditionalData = $"Maximum page size: {MaximumPageSize}"
});
var page = pageQuery ?? 1;
var paginationResult = await queryGenerator().ConfigureAwait(false);
if (paginationResult.EarlyOut != null)
return paginationResult.EarlyOut;
var queriedResults = paginationResult
.Results
.Skip((page - 1) * pageSize)
.Take(pageSize);
int totalResults;
List<TModel> pagedResults;
if (queriedResults.Provider is IAsyncQueryProvider)
{
totalResults = await paginationResult.Results.CountAsync(cancellationToken).ConfigureAwait(false);
pagedResults = await queriedResults
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
}
else
{
totalResults = paginationResult.Results.Count();
pagedResults = queriedResults.ToList();
}
if (resultTransformer != null)
foreach (var I in pagedResults)
resultTransformer(I);
ICollection<TResultModel> finalResults;
if (typeof(TModel) == typeof(TResultModel))
finalResults = (List<TResultModel>)(object)pagedResults; // clearly a safe cast
else
finalResults = pagedResults
.OfType<IApiTransformable<TResultModel>>()
.Select(x => x.ToApi())
.ToList();
return Json(
new Paginated<TModel>
{
Content = pagedResults,
PageSize = pageSize,
TotalPages = (ushort)((totalResults % pageSize) + 1)
});
}
}
}
@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
@@ -79,21 +78,31 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// Lists installed <see cref="Api.Models.Byond"/> versions.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
/// <response code="200">Retrieved version information successfully.</response>
[HttpGet(Routes.List)]
[TgsAuthorize(ByondRights.ListInstalled)]
[ProducesResponseType(typeof(IEnumerable<Api.Models.Byond>), 200)]
public Task<IActionResult> List()
=> WithComponentInstance(instance =>
Task.FromResult<IActionResult>(
Json(instance
.ByondManager
.InstalledVersions
.Select(x => new Api.Models.Byond
{
Version = x
}))));
[ProducesResponseType(typeof(Paginated<Api.Models.Byond>), 200)]
public Task<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
=> WithComponentInstance(
instance => Paginated(
() => Task.FromResult(
new PaginatableResult<Api.Models.Byond>(
instance
.ByondManager
.InstalledVersions
.Select(x => new Api.Models.Byond
{
Version = x
})
.AsQueryable())),
null,
page,
pageSize,
cancellationToken));
/// <summary>
/// Changes the active BYOND version to the one specified in a given <paramref name="model"/>.
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
@@ -168,29 +168,33 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// List <see cref="Api.Models.ChatBot"/>s.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
/// <response code="200">Listed chat bots successfully.</response>
[HttpGet(Routes.List)]
[TgsAuthorize(ChatBotRights.Read)]
[ProducesResponseType(typeof(IEnumerable<Api.Models.ChatBot>), 200)]
public async Task<IActionResult> List(CancellationToken cancellationToken)
[ProducesResponseType(typeof(Paginated<Api.Models.ChatBot>), 200)]
public Task<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
{
var query = DatabaseContext
.ChatBots
.AsQueryable()
.Where(x => x.InstanceId == Instance.Id)
.Include(x => x.Channels);
var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
if (!connectionStrings)
foreach (var I in results)
I.ConnectionString = null;
return Json(results.Select(x => x.ToApi()));
return Paginated<Models.ChatBot, Api.Models.ChatBot>(
() => Task.FromResult(
new PaginatableResult<Models.ChatBot>(
DatabaseContext
.ChatBots
.AsQueryable()
.Where(x => x.InstanceId == Instance.Id)
.Include(x => x.Channels))),
chatBot =>
{
if (connectionStrings)
chatBot.ConnectionString = null;
},
page,
pageSize,
cancellationToken);
}
/// <summary>
@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -172,54 +171,75 @@ namespace Tgstation.Server.Host.Controllers
/// Get the contents of a directory at a <paramref name="directoryPath"/>
/// </summary>
/// <param name="directoryPath">The path of the directory to get</param>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation</returns>
/// <response code="200">Directory listed successfully.</response>>
/// <response code="410">Directory does not currently exist.</response>
[HttpGet(Routes.List + "/{*directoryPath}")]
[TgsAuthorize(ConfigurationRights.List)]
[ProducesResponseType(typeof(IReadOnlyList<ConfigurationFile>), 200)]
[ProducesResponseType(typeof(Paginated<ConfigurationFile>), 200)]
[ProducesResponseType(typeof(ErrorMessage), 410)]
public async Task<IActionResult> Directory(string directoryPath, CancellationToken cancellationToken)
{
if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity))
return Forbid();
public Task<IActionResult> Directory(
string directoryPath,
[FromQuery] int? page,
[FromQuery] int? pageSize,
CancellationToken cancellationToken)
=> Paginated(
async () =>
{
if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity))
return new PaginatableResult<ConfigurationFile>(
Forbid());
try
{
return await WithComponentInstance(
async instance =>
try
{
var result = await instance
.Configuration
.ListDirectory(directoryPath, systemIdentity, cancellationToken)
.ConfigureAwait(false);
if (result == null)
return Gone();
return new PaginatableResult<ConfigurationFile>(
await WithComponentInstance(
async instance =>
{
var result = await instance
.Configuration
.ListDirectory(directoryPath, systemIdentity, cancellationToken)
.ConfigureAwait(false);
if (result == null)
return Gone();
return Json(result);
})
.ConfigureAwait(false);
}
catch (NotImplementedException)
{
return RequiresPosixSystemIdentity();
}
catch (UnauthorizedAccessException)
{
return Forbid();
}
}
return Json(result);
})
.ConfigureAwait(false));
}
catch (NotImplementedException)
{
return new PaginatableResult<ConfigurationFile>(
RequiresPosixSystemIdentity());
}
catch (UnauthorizedAccessException)
{
return new PaginatableResult<ConfigurationFile>(
Forbid());
}
},
null,
page,
pageSize,
cancellationToken);
/// <summary>
/// Get the contents of the root configuration directory.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
[HttpGet(Routes.List)]
[TgsAuthorize(ConfigurationRights.List)]
[ProducesResponseType(typeof(IReadOnlyList<ConfigurationFile>), 200)]
public Task<IActionResult> List(CancellationToken cancellationToken) => Directory(null, cancellationToken);
[ProducesResponseType(typeof(Paginated<ConfigurationFile>), 200)]
public Task<IActionResult> List(
[FromQuery] int? page,
[FromQuery] int? pageSize,
CancellationToken cancellationToken) => Directory(null, page, pageSize, cancellationToken);
/// <summary>
/// Create a configuration directory.
@@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -95,43 +94,53 @@ namespace Tgstation.Server.Host.Controllers
[ProducesResponseType(typeof(ErrorMessage), 404)]
public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
{
var compileJob = await DatabaseContext
.CompileJobs
.AsQueryable()
.Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
.Include(x => x.Job).ThenInclude(x => x.StartedBy)
.Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
.Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
var compileJob = await BaseCompileJobsQuery()
.Where(x => x.Id == id)
.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (compileJob == default)
return NotFound();
return Json(compileJob.ToApi());
}
/// <summary>
/// Base query for pulling in all required <see cref="CompileJob"/> fields.
/// </summary>
/// <returns>An <see cref="IQueryable{T}"/> of <see cref="CompileJob"/> with all the inclusions.</returns>
IQueryable<Models.CompileJob> BaseCompileJobsQuery() => DatabaseContext
.CompileJobs
.AsQueryable()
.Include(x => x.Job)
.ThenInclude(x => x.StartedBy)
.Include(x => x.RevisionInformation)
.ThenInclude(x => x.PrimaryTestMerge)
.ThenInclude(x => x.MergedBy)
.Include(x => x.RevisionInformation)
.ThenInclude(x => x.ActiveTestMerges)
.ThenInclude(x => x.TestMerge)
.ThenInclude(x => x.MergedBy)
.Where(x => x.Job.Instance.Id == Instance.Id);
/// <summary>
/// List all <see cref="Api.Models.CompileJob"/> <see cref="EntityId"/>s for the instance.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request.</returns>
/// <response code="200">Retrieved <see cref="EntityId"/>s successfully.</response>
[HttpGet(Routes.List)]
[TgsAuthorize(DreamMakerRights.CompileJobs)]
[ProducesResponseType(typeof(List<EntityId>), 200)]
public async Task<IActionResult> List(CancellationToken cancellationToken)
{
var compileJobs = await DatabaseContext
.CompileJobs
.AsQueryable()
.Where(x => x.Job.Instance.Id == Instance.Id)
.OrderByDescending(x => x.Job.StoppedAt)
.Select(x => new EntityId
{
Id = x.Id
})
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return Json(compileJobs);
}
[ProducesResponseType(typeof(Paginated<Api.Models.CompileJob>), 200)]
public Task<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
=> Paginated<Models.CompileJob, Api.Models.CompileJob>(
() => Task.FromResult(
new PaginatableResult<Models.CompileJob>(
BaseCompileJobsQuery()
.OrderByDescending(x => x.Job.StoppedAt))),
null,
page,
pageSize,
cancellationToken);
/// <summary>
/// Begin deploying repository code.
@@ -586,13 +586,18 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// List <see cref="Api.Models.Instance"/>s.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request.</returns>
/// <response code="200">Retrieved <see cref="Api.Models.Instance"/>s successfully.</response>
[HttpGet(Routes.List)]
[TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
[ProducesResponseType(typeof(IEnumerable<Api.Models.Instance>), 200)]
public async Task<IActionResult> List(CancellationToken cancellationToken)
[ProducesResponseType(typeof(Paginated<Api.Models.Instance>), 200)]
public async Task<IActionResult> List(
[FromQuery] int? page,
[FromQuery] int? pageSize,
CancellationToken cancellationToken)
{
IQueryable<Models.Instance> GetBaseQuery()
{
@@ -622,21 +627,25 @@ namespace Tgstation.Server.Host.Controllers
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var instances = await GetBaseQuery()
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var needsUpdate = false;
foreach (var instance in instances)
needsUpdate |= InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance);
var result = await Paginated<Models.Instance, Api.Models.Instance>(
() => Task.FromResult(
new PaginatableResult<Models.Instance>(
GetBaseQuery())),
instance =>
{
needsUpdate |= InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance);
instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance.Id == instance.Id).ToApi();
},
page,
pageSize,
cancellationToken)
.ConfigureAwait(false);
if (needsUpdate)
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
var apis = instances.Select(x => x.ToApi());
foreach(var I in moveJobs)
apis.Where(x => x.Id == I.Instance.Id).First().MoveJob = I.ToApi(); // if this .First() fails i will personally murder kevinz000 because I just know he is somehow responsible
return Json(apis);
return result;
}
/// <summary>
@@ -1,8 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -147,23 +146,27 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// Lists <see cref="Api.Models.InstanceUser"/>s for the instance.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request.</returns>
/// <response code="200">Retrieved <see cref="Api.Models.InstanceUser"/>s successfully.</response>
[HttpGet(Routes.List)]
[TgsAuthorize(InstanceUserRights.ReadUsers)]
[ProducesResponseType(typeof(IEnumerable<Api.Models.InstanceUser>), 200)]
public async Task<IActionResult> List(CancellationToken cancellationToken)
{
var users = await DatabaseContext
.Instances
.AsQueryable()
.Where(x => x.Id == Instance.Id)
.SelectMany(x => x.InstanceUsers)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return Json(users.Select(x => x.ToApi()));
}
[ProducesResponseType(typeof(Paginated<Api.Models.InstanceUser>), 200)]
public Task<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
=> Paginated<Models.InstanceUser, Api.Models.InstanceUser>(
() => Task.FromResult(
new PaginatableResult<Models.InstanceUser>(
DatabaseContext
.Instances
.AsQueryable()
.Where(x => x.Id == Instance.Id)
.SelectMany(x => x.InstanceUsers))),
null,
page,
pageSize,
cancellationToken);
/// <summary>
/// Gets a specific <see cref="Api.Models.InstanceUser"/>.
@@ -1,8 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -53,50 +52,54 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// Get active <see cref="Api.Models.Job"/>s for the instance.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request.</returns>
/// <response code="200">Retrieved active <see cref="Api.Models.Job"/>s successfully.</response>
[HttpGet]
[TgsAuthorize]
[ProducesResponseType(typeof(IEnumerable<Api.Models.Job>), 200)]
public async Task<IActionResult> Read(CancellationToken cancellationToken)
{
var result = await DatabaseContext
.Jobs
.AsQueryable()
.Include(x => x.StartedBy)
.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue)
.OrderByDescending(x => x.StartedAt)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return Json(result.Select(x => x.ToApi()));
}
[ProducesResponseType(typeof(Paginated<Api.Models.Job>), 200)]
public Task<IActionResult> Read([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
=> Paginated<Models.Job, Api.Models.Job>(
() => Task.FromResult(
new PaginatableResult<Models.Job>(
DatabaseContext
.Jobs
.AsQueryable()
.Include(x => x.StartedBy)
.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue)
.OrderByDescending(x => x.StartedAt))),
null,
page,
pageSize,
cancellationToken);
/// <summary>
/// List all <see cref="Api.Models.Job"/> <see cref="EntityId"/>s for the instance in reverse creation order.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request.</returns>
/// <response code="200">Retrieved <see cref="Api.Models.Job"/> <see cref="EntityId"/>s successfully.</response>
[HttpGet(Routes.List)]
[TgsAuthorize]
[ProducesResponseType(typeof(List<EntityId>), 200)]
public async Task<IActionResult> List(CancellationToken cancellationToken)
{
// you KNOW this will need pagination eventually right?
var jobs = await DatabaseContext
.Jobs
.AsQueryable()
.Where(x => x.Instance.Id == Instance.Id)
.OrderByDescending(x => x.StartedAt)
.Select(x => new EntityId
{
Id = x.Id
})
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return Json(jobs);
}
[ProducesResponseType(typeof(Paginated<Api.Models.Job>), 200)]
public Task<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
=> Paginated<Models.Job, Api.Models.Job>(
() => Task.FromResult(
new PaginatableResult<Models.Job>(
DatabaseContext
.Jobs
.AsQueryable()
.Include(x => x.StartedBy)
.Where(x => x.Instance.Id == Instance.Id)
.OrderByDescending(x => x.StartedAt))),
null,
page,
pageSize,
cancellationToken);
/// <summary>
/// Cancel a running <see cref="Api.Models.Job"/>.
@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Mvc;
using System;
using System.Linq;
namespace Tgstation.Server.Host.Controllers
{
/// <summary>
/// Helper for returning paginated models.
/// </summary>
/// <typeparam name="TModel">The <see cref="Type"/> of model intended to be returned.</typeparam>
public sealed class PaginatableResult<TModel>
{
/// <summary>
/// The <see cref="IQueryable{T}"/> <typeparamref name="TModel"/> results.
/// </summary>
public IQueryable<TModel> Results { get; }
/// <summary>
/// An <see cref="IActionResult"/> to return immediately.
/// </summary>
public IActionResult EarlyOut { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PaginatableResult{TModel}"/> <see langword="class"/>.
/// </summary>
/// <param name="results">The value of <see cref="Results"/>.</param>
public PaginatableResult(IQueryable<TModel> results)
{
Results = results ?? throw new ArgumentNullException(nameof(results));
}
/// <summary>
/// Initializes a new instance of the <see cref="PaginatableResult{TModel}"/> <see langword="class"/>.
/// </summary>
/// <param name="earlyOut">The value of <see cref="EarlyOut"/>.</param>
public PaginatableResult(IActionResult earlyOut)
{
EarlyOut = earlyOut ?? throw new ArgumentNullException(nameof(earlyOut));
}
}
}
@@ -298,23 +298,28 @@ namespace Tgstation.Server.Host.Controllers
/// <summary>
/// List all <see cref="Api.Models.User"/>s in the server.
/// </summary>
/// <param name="page">The current page.</param>
/// <param name="pageSize">The page size.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation.</returns>
/// <response code="200">Retrieved <see cref="Api.Models.User"/>s successfully.</response>
[HttpGet(Routes.List)]
[TgsAuthorize(AdministrationRights.ReadUsers)]
[ProducesResponseType(typeof(IEnumerable<Api.Models.User>), 200)]
public async Task<IActionResult> List(CancellationToken cancellationToken)
{
var users = await DatabaseContext
.Users
.AsQueryable()
.Where(x => x.CanonicalName != Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
.Include(x => x.CreatedBy)
.Include(x => x.OAuthConnections)
.ToListAsync(cancellationToken).ConfigureAwait(false);
return Json(users.Select(x => x.ToApi(true)));
}
[ProducesResponseType(typeof(Paginated<Api.Models.User>), 200)]
public Task<IActionResult> List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken)
=> Paginated<Models.User, Api.Models.User>(
() => Task.FromResult(
new PaginatableResult<Models.User>(
DatabaseContext
.Users
.AsQueryable()
.Where(x => x.CanonicalName != Models.User.CanonicalizeName(Models.User.TgsSystemUserName))
.Include(x => x.CreatedBy)
.Include(x => x.OAuthConnections))),
null,
page,
pageSize,
cancellationToken);
/// <summary>
/// Get a specific <see cref="Api.Models.User"/>.
@@ -164,6 +164,9 @@ namespace Tgstation.Server.Host.Core
if (type == typeof(Api.Models.Internal.User))
return "ShallowUser";
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Paginated<>))
return $"Paginated{type.GenericTypeArguments.First().Name}";
return type.Name;
});
@@ -241,7 +244,7 @@ namespace Tgstation.Server.Host.Core
};
if (typeof(InstanceRequiredController).IsAssignableFrom(context.MethodInfo.DeclaringType))
operation.Parameters.Add(new OpenApiParameter
operation.Parameters.Insert(0, new OpenApiParameter
{
Reference = new OpenApiReference
{
+3 -6
View File
@@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class ChatBot : Api.Models.Internal.ChatBot
public sealed class ChatBot : Api.Models.Internal.ChatBot, IApiTransformable<Api.Models.ChatBot>
{
/// <summary>
/// Default for <see cref="Api.Models.Internal.ChatBot.ChannelLimit"/>.
@@ -28,10 +28,7 @@ namespace Tgstation.Server.Host.Models
/// </summary>
public ICollection<ChatChannel> Channels { get; set; }
/// <summary>
/// Convert the <see cref="ChatBot"/> to it's API form
/// </summary>
/// <returns>A new <see cref="Api.Models.ChatBot"/></returns>
/// <inheritdoc />
public Api.Models.ChatBot ToApi() => new Api.Models.ChatBot
{
Channels = Channels.Select(x => x.ToApi()).ToList(),
@@ -1,7 +1,7 @@
namespace Tgstation.Server.Host.Models
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class ChatChannel : Api.Models.ChatChannel
public sealed class ChatChannel : Api.Models.ChatChannel, IApiTransformable<Api.Models.ChatChannel>
{
/// <summary>
/// The row Id
@@ -18,10 +18,7 @@
/// </summary>
public ChatBot ChatSettings { get; set; }
/// <summary>
/// Convert the <see cref="ChatChannel"/> to it's API form
/// </summary>
/// <returns>A new <see cref="Api.Models.ChatChannel"/></returns>
/// <inheritdoc />
public Api.Models.ChatChannel ToApi() => new Api.Models.ChatChannel
{
DiscordChannelId = DiscordChannelId,
@@ -5,7 +5,7 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class CompileJob : Api.Models.Internal.CompileJob
public sealed class CompileJob : Api.Models.Internal.CompileJob, IApiTransformable<Api.Models.CompileJob>
{
/// <summary>
/// See <see cref="Api.Models.CompileJob.Job"/>
@@ -78,10 +78,7 @@ namespace Tgstation.Server.Host.Models
}
}
/// <summary>
/// Convert the <see cref="CompileJob"/> to it's API form
/// </summary>
/// <returns>A new <see cref="Api.Models.CompileJob"/></returns>
/// <inheritdoc />
public Api.Models.CompileJob ToApi() => new Api.Models.CompileJob
{
DirectoryName = DirectoryName,
@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class DreamMakerSettings : Api.Models.DreamMaker
public sealed class DreamMakerSettings : Api.Models.DreamMaker, IApiTransformable<Api.Models.DreamMaker>
{
/// <summary>
/// The row Id
@@ -21,10 +21,7 @@ namespace Tgstation.Server.Host.Models
[Required]
public Instance Instance { get; set; }
/// <summary>
/// Convert the <see cref="DreamDaemonSettings"/> to it's API form
/// </summary>
/// <returns>A new <see cref="Api.Models.DreamMaker"/></returns>
/// <inheritdoc />
public Api.Models.DreamMaker ToApi() => new Api.Models.DreamMaker
{
ProjectName = ProjectName,
@@ -0,0 +1,15 @@
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// Represents a host-side model that may be transformed into a <typeparamref name="TApiModel"/>.
/// </summary>
/// <typeparam name="TApiModel">The API form of the model.</typeparam>
public interface IApiTransformable<TApiModel>
{
/// <summary>
/// Convert the <see cref="IApiTransformable{TApiModel}"/> to it's <typeparamref name="TApiModel"/>.
/// </summary>
/// <returns>A new <typeparamref name="TApiModel"/> based on the <see cref="IApiTransformable{TApiModel}"/>.</returns>
TApiModel ToApi();
}
}
+5 -7
View File
@@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.Collections.Generic;
namespace Tgstation.Server.Host.Models
{
/// <summary>
/// Represents an <see cref="Api.Models.Instance"/> in the database
/// </summary>
public sealed class Instance : Api.Models.Instance
public sealed class Instance : Api.Models.Instance, IApiTransformable<Api.Models.Instance>
{
/// <summary>
/// Default for <see cref="Api.Models.Instance.ChatBotLimit"/>.
@@ -47,10 +47,7 @@ namespace Tgstation.Server.Host.Models
/// </summary>
public ICollection<Job> Jobs { get; set; }
/// <summary>
/// Convert the <see cref="Instance"/> to it's API form
/// </summary>
/// <returns>A new <see cref="Api.Models.Instance"/></returns>
/// <inheritdoc />
public Api.Models.Instance ToApi() => new Api.Models.Instance
{
AutoUpdateInterval = AutoUpdateInterval,
@@ -59,7 +56,8 @@ namespace Tgstation.Server.Host.Models
Name = Name,
Path = Path,
Online = Online,
ChatBotLimit = ChatBotLimit
ChatBotLimit = ChatBotLimit,
MoveJob = MoveJob,
};
}
}
@@ -1,9 +1,9 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class InstanceUser : Api.Models.InstanceUser
public sealed class InstanceUser : Api.Models.InstanceUser, IApiTransformable<Api.Models.InstanceUser>
{
/// <summary>
/// The row Id
@@ -21,10 +21,7 @@ namespace Tgstation.Server.Host.Models
[Required]
public Instance Instance { get; set; }
/// <summary>
/// Convert the <see cref="InstanceUser"/> to it's API form
/// </summary>
/// <returns>A new <see cref="Api.Models.InstanceUser"/></returns>
/// <inheritdoc />
public Api.Models.InstanceUser ToApi() => new Api.Models.InstanceUser
{
ByondRights = ByondRights,
+2 -5
View File
@@ -4,7 +4,7 @@ namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
#pragma warning disable CA1724 // naming conflict with gitlab package
public sealed class Job : Api.Models.Internal.Job
public sealed class Job : Api.Models.Internal.Job, IApiTransformable<Api.Models.Job>
#pragma warning restore CA1724
{
/// <summary>
@@ -24,10 +24,7 @@ namespace Tgstation.Server.Host.Models
[Required]
public Instance Instance { get; set; }
/// <summary>
/// Convert the <see cref="Job"/> to it's API form
/// </summary>
/// <returns>A new <see cref="Api.Models.Job"/></returns>
/// <inheritdoc />
public Api.Models.Job ToApi() => new Api.Models.Job
{
Id = Id,
@@ -1,7 +1,7 @@
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class OAuthConnection : Api.Models.OAuthConnection
public sealed class OAuthConnection : Api.Models.OAuthConnection, IApiTransformable<Api.Models.OAuthConnection>
{
/// <summary>
/// The row Id.
@@ -13,10 +13,7 @@ namespace Tgstation.Server.Host.Models
/// </summary>
public User User { get; set; }
/// <summary>
/// Convert the <see cref="OAuthConnection"/> to it's API form.
/// </summary>
/// <returns>A new <see cref="Api.Models.OAuthConnection"/>.</returns>
/// <inheritdoc />
public Api.Models.OAuthConnection ToApi() => new Api.Models.OAuthConnection
{
Provider = Provider,
@@ -4,7 +4,7 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings
public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings, IApiTransformable<Repository>
{
/// <summary>
/// The row Id
@@ -22,10 +22,7 @@ namespace Tgstation.Server.Host.Models
[Required]
public Instance Instance { get; set; }
/// <summary>
/// Convert the <see cref="Repository"/> to it's API form
/// </summary>
/// <returns>A new <see cref="Repository"/></returns>
/// <inheritdoc />
public Repository ToApi() => new Repository
{
// AccessToken = AccessToken, // never show this
@@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation
public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation, IApiTransformable<Api.Models.RevisionInformation>
{
/// <summary>
/// The row Id
@@ -38,10 +38,7 @@ namespace Tgstation.Server.Host.Models
/// </summary>
public ICollection<CompileJob> CompileJobs { get; set; }
/// <summary>
/// Convert the <see cref="RevisionInformation"/> to it's API form
/// </summary>
/// <returns>A new <see cref="Api.Models.RevisionInformation"/></returns>
/// <inheritdoc />
public Api.Models.RevisionInformation ToApi() => new Api.Models.RevisionInformation
{
CommitSha = CommitSha,
@@ -1,10 +1,10 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class TestMerge : Api.Models.Internal.TestMerge
public sealed class TestMerge : Api.Models.Internal.TestMerge, IApiTransformable<Api.Models.TestMerge>
{
/// <summary>
/// See <see cref="Api.Models.TestMerge.MergedBy"/>
@@ -28,10 +28,7 @@ namespace Tgstation.Server.Host.Models
/// </summary>
public ICollection<RevInfoTestMerge> RevisonInformations { get; set; }
/// <summary>
/// Convert the <see cref="TestMerge"/> to it's API form
/// </summary>
/// <returns>A new <see cref="Api.Models.TestMerge"/></returns>
/// <inheritdoc />
public Api.Models.TestMerge ToApi() => new Api.Models.TestMerge
{
Author = Author,
+4 -1
View File
@@ -6,7 +6,7 @@ using System.Linq;
namespace Tgstation.Server.Host.Models
{
/// <inheritdoc />
public sealed class User : Api.Models.Internal.User
public sealed class User : Api.Models.Internal.User, IApiTransformable<Api.Models.User>
{
/// <summary>
/// Username used when creating jobs automatically.
@@ -88,5 +88,8 @@ namespace Tgstation.Server.Host.Models
/// <param name="showDetails">If rights and system identifier should be shown</param>
/// <returns>A new <see cref="Api.Models.User"/></returns>
public Api.Models.User ToApi(bool showDetails) => ToApi(true, showDetails);
/// <inheritdoc />
public Api.Models.User ToApi() => ToApi(true);
}
}
@@ -28,7 +28,7 @@ namespace Tgstation.Server.Tests
async Task TestLogs(CancellationToken cancellationToken)
{
var logs = await client.ListLogs(cancellationToken);
var logs = await client.ListLogs(null, cancellationToken);
Assert.AreNotEqual(0, logs.Count);
var logFile = logs.First();
Assert.IsNotNull(logFile);
@@ -75,7 +75,7 @@ namespace Tgstation.Server.Tests.Instance
async Task TestNoVersion(CancellationToken cancellationToken)
{
var allVersionsTask = byondClient.InstalledVersions(cancellationToken);
var allVersionsTask = byondClient.InstalledVersions(null, cancellationToken);
var currentShit = await byondClient.ActiveVersion(cancellationToken).ConfigureAwait(false);
Assert.IsNotNull(currentShit);
Assert.IsNull(currentShit.InstallJob);
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Tests.Instance
Assert.AreEqual(csb.ToString(), firstBot.ConnectionString);
Assert.AreNotEqual(0, firstBot.Id);
var bots = await chatClient.List(cancellationToken);
var bots = await chatClient.List(null, cancellationToken);
Assert.AreEqual(firstBot.Id, bots.First(x => x.Provider.Value == ChatProvider.Irc).Id);
var retrievedBot = await chatClient.GetId(firstBot, cancellationToken);
@@ -115,7 +115,7 @@ namespace Tgstation.Server.Tests.Instance
Assert.AreEqual(csb.ToString(), firstBot.ConnectionString);
Assert.AreNotEqual(0, firstBot.Id);
var bots = await chatClient.List(cancellationToken);
var bots = await chatClient.List(null, cancellationToken);
Assert.AreEqual(firstBot.Id, bots.First(x => x.Provider.Value == ChatProvider.Discord).Id);
var retrievedBot = await chatClient.GetId(firstBot, cancellationToken);
@@ -154,13 +154,13 @@ namespace Tgstation.Server.Tests.Instance
public async Task RunPostTest(CancellationToken cancellationToken)
{
var activeBots = await chatClient.List(cancellationToken);
var activeBots = await chatClient.List(null, cancellationToken);
Assert.AreEqual(2, activeBots.Count);
await Task.WhenAll(activeBots.Select(bot => chatClient.Delete(bot, cancellationToken)));
var nowBots = await chatClient.List(cancellationToken);
var nowBots = await chatClient.List(null, cancellationToken);
Assert.AreEqual(0, nowBots.Count);
}
@@ -173,7 +173,7 @@ namespace Tgstation.Server.Tests.Instance
Provider = ChatProvider.Irc
}, cancellationToken), ErrorCode.ChatBotMax);
var bots = await chatClient.List(cancellationToken);
var bots = await chatClient.List(null, cancellationToken);
var discordBot = bots.First(bot => bot.Provider.Value == ChatProvider.Discord);
// We limited chat bots and channels to 1 and 2 respectively, try violating them
@@ -1,4 +1,4 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Linq;
@@ -137,7 +137,7 @@ namespace Tgstation.Server.Tests
public async Task RunPostTest(CancellationToken cancellationToken)
{
var instances = await instanceManagerClient.List(cancellationToken);
var instances = await instanceManagerClient.List(null, cancellationToken);
var firstTest = instances.Single(x => x.Name == TestInstanceName);
var instanceClient = instanceManagerClient.CreateClient(firstTest);
@@ -320,10 +320,10 @@ namespace Tgstation.Server.Tests
{
var instanceClient = adminClient.Instances.CreateClient(instance);
var jobs = await instanceClient.Jobs.ListActive(cancellationToken);
var jobs = await instanceClient.Jobs.ListActive(null, cancellationToken);
if (!jobs.Any())
{
var entities = await instanceClient.Jobs.List(cancellationToken);
var entities = await instanceClient.Jobs.List(null, cancellationToken);
var getTasks = entities
.Select(e => instanceClient.Jobs.GetId(e, cancellationToken))
.ToList();
@@ -363,10 +363,10 @@ namespace Tgstation.Server.Tests
{
var instanceClient = adminClient.Instances.CreateClient(instance);
var jobs = await instanceClient.Jobs.ListActive(cancellationToken);
var jobs = await instanceClient.Jobs.ListActive(null, cancellationToken);
if (!jobs.Any())
{
var entities = await instanceClient.Jobs.List(cancellationToken);
var entities = await instanceClient.Jobs.List(null, cancellationToken);
var getTasks = entities
.Select(e => instanceClient.Jobs.GetId(e, cancellationToken))
.ToList();
+53 -1
View File
@@ -25,6 +25,8 @@ namespace Tgstation.Server.Tests
BasicTests(cancellationToken),
TestCreateSysUser(cancellationToken),
TestSpamCreation(cancellationToken)).ConfigureAwait(false);
await TestPagination(cancellationToken);
}
async Task BasicTests(CancellationToken cancellationToken)
@@ -41,7 +43,7 @@ namespace Tgstation.Server.Tests
Assert.AreEqual("TGS", systemUser.Name);
Assert.AreEqual(false, systemUser.Enabled);
var users = await client.List(cancellationToken);
var users = await client.List(null, cancellationToken);
Assert.IsTrue(users.Count > 0);
Assert.IsFalse(users.Any(x => x.Id == systemUser.Id));
@@ -133,5 +135,55 @@ namespace Tgstation.Server.Tests
Assert.AreEqual(RepeatCount, tasks.Select(task => task.Result.Id).Distinct().Count(), "Did not receive expected number of unique user IDs!");
}
async Task TestPagination(CancellationToken cancellationToken)
{
// we test pagination here b/c it's the only spot we have a decent amount of entities
var nullSettings = await client.List(null, cancellationToken);
var emptySettings = await client.List(
new PaginationSettings
{
}, cancellationToken);
Assert.AreEqual(nullSettings.Count, emptySettings.Count);
Assert.IsTrue(nullSettings.All(x => emptySettings.SingleOrDefault(y => x.Id == y.Id) != null));
await ApiAssert.ThrowsException<ApiConflictException>(() => client.List(
new PaginationSettings
{
PageSize = -2143
}, cancellationToken), ErrorCode.ApiInvalidPageOrPageSize);
await ApiAssert.ThrowsException<ApiConflictException>(() => client.List(
new PaginationSettings
{
PageSize = Int32.MaxValue
}, cancellationToken), ErrorCode.ApiPageTooLarge);
await client.List(
new PaginationSettings
{
PageSize = 50
},
cancellationToken);
var skipped = await client.List(new PaginationSettings
{
Offset = 50,
RetrieveCount = 5
}, cancellationToken);
Assert.AreEqual(5, skipped.Count);
var allAfterSkipped = await client.List(new PaginationSettings
{
Offset = 50,
}, cancellationToken);
Assert.IsTrue(5 < allAfterSkipped.Count);
var limited = await client.List(new PaginationSettings
{
RetrieveCount = 12,
}, cancellationToken);
Assert.AreEqual(12, limited.Count);
}
}
}