Merge pull request 'feat(api): REST CRUD for multi-collections and rerun-collections (#151, #152)' (#188) from feat/multi-rerun-collections-api into main
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 4m36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 4m39s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m55s

This commit was merged in pull request #188.
This commit is contained in:
2026-07-08 16:54:35 +00:00
18 changed files with 2248 additions and 3 deletions
@@ -0,0 +1,20 @@
#nullable enable
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Api.MediaCollections;
public record MultiCollectionResponseModel(
int Id,
string Name,
List<MultiCollectionItemResponseModel> Items);
public record MultiCollectionItemResponseModel(
int? CollectionId,
int? SmartCollectionId,
string Name,
bool ScheduleAsGroup,
PlaybackOrder PlaybackOrder);
public record PagedMultiCollectionsResponseModel(
int TotalCount,
List<MultiCollectionResponseModel> Page);
@@ -0,0 +1,17 @@
#nullable enable
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Api.MediaCollections;
public record RerunCollectionResponseModel(
int Id,
string Name,
CollectionType CollectionType,
int? SelectedId,
string? SelectedName,
PlaybackOrder FirstRunPlaybackOrder,
PlaybackOrder RerunPlaybackOrder);
public record PagedRerunCollectionsResponseModel(
int TotalCount,
List<RerunCollectionResponseModel> Page);
@@ -0,0 +1,220 @@
using System.Reflection;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.MediaCollections;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Tests.Controllers;
[TestFixture]
public class MultiCollectionControllerTests
{
[SetUp]
public void SetUp()
{
_mediator = Substitute.For<IMediator>();
_controller = new MultiCollectionController(_mediator);
}
private MultiCollectionController _controller = null!;
private IMediator _mediator = null!;
[Test]
public void Controller_Should_Expose_Idiomatic_Rest_Routes()
{
ShouldHaveActionRoute(nameof(MultiCollectionController.GetAll), "GET", "/api/multi-collections");
ShouldHaveActionRoute(nameof(MultiCollectionController.GetById), "GET", "/api/multi-collections/{id:int}");
ShouldHaveActionRoute(nameof(MultiCollectionController.Create), "POST", "/api/multi-collections");
ShouldHaveActionRoute(nameof(MultiCollectionController.Update), "PUT", "/api/multi-collections/{id:int}");
ShouldHaveActionRoute(nameof(MultiCollectionController.Delete), "DELETE", "/api/multi-collections/{id:int}");
}
[Test]
public async Task GetAll_Should_Clamp_Paging_And_Map_Response()
{
_mediator.Send(Arg.Any<GetPagedMultiCollections>(), Arg.Any<CancellationToken>())
.Returns(new PagedMultiCollectionsViewModel(1, new List<MultiCollectionViewModel> { MakeMulti(3, "Kids") }));
PagedMultiCollectionsResponseModel result =
await _controller.GetAll("k", -5, 9999, CancellationToken.None);
result.TotalCount.ShouldBe(1);
result.Page.Count.ShouldBe(1);
result.Page[0].Id.ShouldBe(3);
result.Page[0].Items.Count.ShouldBe(2);
result.Page[0].Items[0].CollectionId.ShouldBe(10);
result.Page[0].Items[1].SmartCollectionId.ShouldBe(20);
await _mediator.Received(1).Send(
Arg.Is<GetPagedMultiCollections>(q => q.Query == "k" && q.PageNum == 0 && q.PageSize == 100),
Arg.Any<CancellationToken>());
}
[Test]
public async Task GetById_Should_Return_200_For_Some()
{
_mediator.Send(Arg.Any<GetMultiCollectionById>(), Arg.Any<CancellationToken>())
.Returns(Option<MultiCollectionViewModel>.Some(MakeMulti(4, "Kids")));
IActionResult result = await _controller.GetById(4, CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<MultiCollectionResponseModel>().Id.ShouldBe(4);
}
[Test]
public async Task GetById_Should_Return_404_For_None()
{
_mediator.Send(Arg.Any<GetMultiCollectionById>(), Arg.Any<CancellationToken>())
.Returns(Option<MultiCollectionViewModel>.None);
IActionResult result = await _controller.GetById(4, CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
}
[Test]
public async Task Create_Should_Return_201_And_Map_Request()
{
_mediator.Send(Arg.Any<CreateMultiCollection>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, MultiCollectionViewModel>(MakeMulti(8, "Kids")));
IActionResult result = await _controller.Create(
new CreateMultiCollectionRequest(
"Kids",
new List<MultiCollectionItemRequest>
{
new(10, null, true, PlaybackOrder.Chronological),
new(null, 20, false, PlaybackOrder.Shuffle)
}),
CancellationToken.None);
var created = result.ShouldBeOfType<CreatedResult>();
created.Location.ShouldBe("/api/multi-collections/8");
created.Value.ShouldBeOfType<MultiCollectionResponseModel>().Name.ShouldBe("Kids");
await _mediator.Received(1).Send(
Arg.Is<CreateMultiCollection>(c =>
c.Name == "Kids" &&
c.Items.Count == 2 &&
c.Items[0].CollectionId == 10 &&
c.Items[0].ScheduleAsGroup &&
c.Items[0].PlaybackOrder == PlaybackOrder.Chronological &&
c.Items[1].SmartCollectionId == 20),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Create_Should_Return_422_On_Validation_Error()
{
_mediator.Send(Arg.Any<CreateMultiCollection>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, MultiCollectionViewModel>(BaseError.New("dupe")));
IActionResult result = await _controller.Create(
new CreateMultiCollectionRequest("Kids", new List<MultiCollectionItemRequest>()),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
}
[Test]
public async Task Update_Should_Return_200_With_Refreshed_Body_And_Map_Request()
{
_mediator.Send(Arg.Any<UpdateMultiCollection>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, Unit>(Unit.Default));
_mediator.Send(Arg.Any<GetMultiCollectionById>(), Arg.Any<CancellationToken>())
.Returns(Option<MultiCollectionViewModel>.Some(MakeMulti(4, "Kids")));
IActionResult result = await _controller.Update(
4,
new UpdateMultiCollectionRequest(
"Kids",
new List<MultiCollectionItemRequest> { new(10, null, true, PlaybackOrder.Random) }),
CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<MultiCollectionResponseModel>().Id.ShouldBe(4);
await _mediator.Received(1).Send(
Arg.Is<UpdateMultiCollection>(c =>
c.MultiCollectionId == 4 &&
c.Name == "Kids" &&
c.Items.Count == 1 &&
c.Items[0].CollectionId == 10 &&
c.Items[0].PlaybackOrder == PlaybackOrder.Random),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Update_Should_Return_422_On_Validation_Error()
{
_mediator.Send(Arg.Any<UpdateMultiCollection>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, Unit>(BaseError.New("bad")));
IActionResult result = await _controller.Update(
4,
new UpdateMultiCollectionRequest("Kids", new List<MultiCollectionItemRequest>()),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<GetMultiCollectionById>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task Delete_Should_Return_204_On_Success()
{
_mediator.Send(Arg.Any<DeleteMultiCollection>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, Unit>(Unit.Default));
IActionResult result = await _controller.Delete(4, CancellationToken.None);
result.ShouldBeOfType<NoContentResult>();
await _mediator.Received(1).Send(
Arg.Is<DeleteMultiCollection>(c => c.MultiCollectionId == 4),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Delete_Should_Return_404_On_NotFoundError()
{
_mediator.Send(Arg.Any<DeleteMultiCollection>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, Unit>(new ErsatzTV.Core.Errors.NotFoundError("missing")));
IActionResult result = await _controller.Delete(4, CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
}
private static MultiCollectionViewModel MakeMulti(int id, string name) =>
new(
id,
name,
new List<MultiCollectionItemViewModel>
{
new(
id,
new MediaCollectionViewModel(CollectionType.Collection, 10, "Movies", false, MediaItemState.Normal),
true,
PlaybackOrder.Chronological)
},
new List<MultiCollectionSmartItemViewModel>
{
new(id, new SmartCollectionViewModel(20, "Smart", "query"), false, PlaybackOrder.Shuffle)
});
private static void ShouldHaveActionRoute(string actionName, string httpMethod, string route)
{
MethodInfo action = typeof(MultiCollectionController).GetMethod(actionName)
?? throw new AssertionException($"Missing action {actionName}");
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
attribute.HttpMethods.ShouldContain(httpMethod);
attribute.Template.ShouldBe(route);
}
}
@@ -144,6 +144,20 @@ public class OpenApiErrorResponseContractTests
[TestCase("/api/smart-collections/{id}", "put", "422")]
[TestCase("/api/smart-collections/{id}", "delete", "404")]
[TestCase("/api/smart-collections/{id}", "delete", "422")]
[TestCase("/api/multi-collections/{id}", "get", "404")]
[TestCase("/api/multi-collections", "post", "404")]
[TestCase("/api/multi-collections", "post", "422")]
[TestCase("/api/multi-collections/{id}", "put", "404")]
[TestCase("/api/multi-collections/{id}", "put", "422")]
[TestCase("/api/multi-collections/{id}", "delete", "404")]
[TestCase("/api/multi-collections/{id}", "delete", "422")]
[TestCase("/api/rerun-collections/{id}", "get", "404")]
[TestCase("/api/rerun-collections", "post", "404")]
[TestCase("/api/rerun-collections", "post", "422")]
[TestCase("/api/rerun-collections/{id}", "put", "404")]
[TestCase("/api/rerun-collections/{id}", "put", "422")]
[TestCase("/api/rerun-collections/{id}", "delete", "404")]
[TestCase("/api/rerun-collections/{id}", "delete", "422")]
[TestCase("/api/schedules/{id}", "get", "404")]
[TestCase("/api/schedules", "post", "404")]
[TestCase("/api/schedules", "post", "422")]
@@ -0,0 +1,288 @@
using System.Reflection;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.MediaCollections;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Tests.Controllers;
[TestFixture]
public class RerunCollectionControllerTests
{
[SetUp]
public void SetUp()
{
_mediator = Substitute.For<IMediator>();
_controller = new RerunCollectionController(_mediator);
}
private RerunCollectionController _controller = null!;
private IMediator _mediator = null!;
[Test]
public void Controller_Should_Expose_Idiomatic_Rest_Routes()
{
ShouldHaveActionRoute(nameof(RerunCollectionController.GetAll), "GET", "/api/rerun-collections");
ShouldHaveActionRoute(nameof(RerunCollectionController.GetById), "GET", "/api/rerun-collections/{id:int}");
ShouldHaveActionRoute(nameof(RerunCollectionController.Create), "POST", "/api/rerun-collections");
ShouldHaveActionRoute(nameof(RerunCollectionController.Update), "PUT", "/api/rerun-collections/{id:int}");
ShouldHaveActionRoute(nameof(RerunCollectionController.Delete), "DELETE", "/api/rerun-collections/{id:int}");
}
[Test]
public async Task GetAll_Should_Clamp_Paging_And_Map_Response()
{
_mediator.Send(Arg.Any<GetPagedRerunCollections>(), Arg.Any<CancellationToken>())
.Returns(new PagedRerunCollectionsViewModel(
1,
new List<RerunCollectionViewModel> { MakeRerunFromSmart(3, "Nightly") }));
PagedRerunCollectionsResponseModel result =
await _controller.GetAll("n", -5, 9999, CancellationToken.None);
result.TotalCount.ShouldBe(1);
result.Page.Count.ShouldBe(1);
result.Page[0].Id.ShouldBe(3);
result.Page[0].CollectionType.ShouldBe(CollectionType.SmartCollection);
result.Page[0].SelectedId.ShouldBe(20);
result.Page[0].SelectedName.ShouldBe("Smart");
await _mediator.Received(1).Send(
Arg.Is<GetPagedRerunCollections>(q => q.Query == "n" && q.PageNum == 0 && q.PageSize == 100),
Arg.Any<CancellationToken>());
}
[Test]
public async Task GetById_Should_Return_200_For_Some()
{
_mediator.Send(Arg.Any<GetRerunCollectionById>(), Arg.Any<CancellationToken>())
.Returns(Option<RerunCollectionViewModel>.Some(MakeRerunFromSmart(4, "Nightly")));
IActionResult result = await _controller.GetById(4, CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<RerunCollectionResponseModel>().Id.ShouldBe(4);
}
[Test]
public async Task GetById_Should_Return_404_For_None()
{
_mediator.Send(Arg.Any<GetRerunCollectionById>(), Arg.Any<CancellationToken>())
.Returns(Option<RerunCollectionViewModel>.None);
IActionResult result = await _controller.GetById(4, CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
}
[Test]
public async Task Create_Should_Return_201_And_Resolve_Selected_Id_Into_Collection_Vm()
{
_mediator.Send(Arg.Any<CreateRerunCollection>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, RerunCollectionViewModel>(MakeRerunFromSmart(8, "Nightly")));
IActionResult result = await _controller.Create(
new CreateRerunCollectionRequest(
"Nightly",
CollectionType.Collection,
42,
PlaybackOrder.Chronological,
PlaybackOrder.Shuffle),
CancellationToken.None);
var created = result.ShouldBeOfType<CreatedResult>();
created.Location.ShouldBe("/api/rerun-collections/8");
await _mediator.Received(1).Send(
Arg.Is<CreateRerunCollection>(c =>
c.Name == "Nightly" &&
c.CollectionType == CollectionType.Collection &&
c.Collection != null && c.Collection.Id == 42 &&
c.MultiCollection == null &&
c.SmartCollection == null &&
c.MediaItem == null &&
c.FirstRunPlaybackOrder == PlaybackOrder.Chronological &&
c.RerunPlaybackOrder == PlaybackOrder.Shuffle),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Create_Should_Resolve_Media_Item_Selection_For_Show_Type()
{
_mediator.Send(Arg.Any<CreateRerunCollection>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, RerunCollectionViewModel>(MakeRerunFromSmart(9, "Nightly")));
await _controller.Create(
new CreateRerunCollectionRequest(
"Nightly",
CollectionType.TelevisionShow,
77,
PlaybackOrder.Chronological,
PlaybackOrder.Chronological),
CancellationToken.None);
await _mediator.Received(1).Send(
Arg.Is<CreateRerunCollection>(c =>
c.CollectionType == CollectionType.TelevisionShow &&
c.Collection == null &&
c.MediaItem != null && c.MediaItem.MediaItemId == 77),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Create_Should_Return_422_On_Validation_Error()
{
_mediator.Send(Arg.Any<CreateRerunCollection>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, RerunCollectionViewModel>(BaseError.New("dupe")));
IActionResult result = await _controller.Create(
new CreateRerunCollectionRequest(
"Nightly",
CollectionType.SmartCollection,
1,
PlaybackOrder.Chronological,
PlaybackOrder.Chronological),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
}
[Test]
public async Task Create_Should_Return_422_For_Unsupported_Collection_Type()
{
IActionResult result = await _controller.Create(
new CreateRerunCollectionRequest(
"Nightly",
CollectionType.Playlist,
1,
PlaybackOrder.Chronological,
PlaybackOrder.Chronological),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<CreateRerunCollection>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task Update_Should_Return_200_With_Refreshed_Body_And_Resolve_Selection()
{
_mediator.Send(Arg.Any<UpdateRerunCollection>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, Unit>(Unit.Default));
_mediator.Send(Arg.Any<GetRerunCollectionById>(), Arg.Any<CancellationToken>())
.Returns(Option<RerunCollectionViewModel>.Some(MakeRerunFromSmart(4, "Nightly")));
IActionResult result = await _controller.Update(
4,
new UpdateRerunCollectionRequest(
"Nightly",
CollectionType.MultiCollection,
55,
PlaybackOrder.Chronological,
PlaybackOrder.Shuffle),
CancellationToken.None);
result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<RerunCollectionResponseModel>().Id.ShouldBe(4);
await _mediator.Received(1).Send(
Arg.Is<UpdateRerunCollection>(c =>
c.RerunCollectionId == 4 &&
c.Name == "Nightly" &&
c.CollectionType == CollectionType.MultiCollection &&
c.MultiCollection != null && c.MultiCollection.Id == 55 &&
c.Collection == null &&
c.SmartCollection == null &&
c.MediaItem == null),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Update_Should_Return_422_On_Validation_Error()
{
_mediator.Send(Arg.Any<UpdateRerunCollection>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, Unit>(BaseError.New("bad")));
IActionResult result = await _controller.Update(
4,
new UpdateRerunCollectionRequest(
"Nightly",
CollectionType.SmartCollection,
1,
PlaybackOrder.Chronological,
PlaybackOrder.Chronological),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<GetRerunCollectionById>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task Update_Should_Return_422_For_Unsupported_Collection_Type()
{
IActionResult result = await _controller.Update(
4,
new UpdateRerunCollectionRequest(
"Nightly",
CollectionType.RerunFirstRun,
1,
PlaybackOrder.Chronological,
PlaybackOrder.Chronological),
CancellationToken.None);
result.ShouldBeOfType<UnprocessableEntityObjectResult>();
await _mediator.DidNotReceive().Send(Arg.Any<UpdateRerunCollection>(), Arg.Any<CancellationToken>());
}
[Test]
public async Task Delete_Should_Return_204_On_Success()
{
_mediator.Send(Arg.Any<DeleteRerunCollection>(), Arg.Any<CancellationToken>())
.Returns(Right<BaseError, Unit>(Unit.Default));
IActionResult result = await _controller.Delete(4, CancellationToken.None);
result.ShouldBeOfType<NoContentResult>();
await _mediator.Received(1).Send(
Arg.Is<DeleteRerunCollection>(c => c.RerunCollectionId == 4),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Delete_Should_Return_404_On_NotFoundError()
{
_mediator.Send(Arg.Any<DeleteRerunCollection>(), Arg.Any<CancellationToken>())
.Returns(Left<BaseError, Unit>(new ErsatzTV.Core.Errors.NotFoundError("missing")));
IActionResult result = await _controller.Delete(4, CancellationToken.None);
result.ShouldBeOfType<NotFoundObjectResult>();
}
private static RerunCollectionViewModel MakeRerunFromSmart(int id, string name) =>
new(
id,
name,
CollectionType.SmartCollection,
null,
null,
new SmartCollectionViewModel(20, "Smart", "query"),
null,
PlaybackOrder.Chronological,
PlaybackOrder.Shuffle);
private static void ShouldHaveActionRoute(string actionName, string httpMethod, string route)
{
MethodInfo action = typeof(RerunCollectionController).GetMethod(actionName)
?? throw new AssertionException($"Missing action {actionName}");
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
attribute.HttpMethods.ShouldContain(httpMethod);
attribute.Template.ShouldBe(route);
}
}
@@ -0,0 +1,129 @@
using System.ComponentModel.DataAnnotations;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.MediaCollections;
using ErsatzTV.Extensions;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers.Api;
[ApiController]
public class MultiCollectionController(IMediator mediator) : ControllerBase
{
private const int MaxPageSize = 100;
[HttpGet("/api/multi-collections", Name = "GetMultiCollections")]
[Tags("Multi Collections")]
[EndpointSummary("Get all multi collections (paged)")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(PagedMultiCollectionsResponseModel), StatusCodes.Status200OK)]
public async Task<PagedMultiCollectionsResponseModel> GetAll(
[FromQuery] string query = "",
[FromQuery] int pageNum = 0,
[FromQuery] int pageSize = 100,
CancellationToken cancellationToken = default)
{
int clampedPageNum = Math.Max(0, pageNum);
int clampedPageSize = Math.Clamp(pageSize, 1, MaxPageSize);
PagedMultiCollectionsViewModel result = await mediator.Send(
new GetPagedMultiCollections(query ?? string.Empty, clampedPageNum, clampedPageSize),
cancellationToken);
return new PagedMultiCollectionsResponseModel(
result.TotalCount,
result.Page.Map(ProjectToResponseModel).ToList());
}
[HttpGet("/api/multi-collections/{id:int}", Name = "GetMultiCollectionById")]
[Tags("Multi Collections")]
[EndpointSummary("Get a multi collection by id")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(MultiCollectionResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
{
Option<MultiCollectionViewModel> result =
await mediator.Send(new GetMultiCollectionById(id), cancellationToken);
return result.Map(ProjectToResponseModel).ToGetResult();
}
[HttpPost("/api/multi-collections")]
[Tags("Multi Collections")]
[EndpointSummary("Create a multi collection")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(MultiCollectionResponseModel), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Create(
[Required] [FromBody] CreateMultiCollectionRequest request,
CancellationToken cancellationToken)
{
Either<BaseError, MultiCollectionViewModel> result =
await mediator.Send(request.ToCommand(), cancellationToken);
return result.ToCreatedResult(
vm => $"/api/multi-collections/{vm.Id}",
ProjectToResponseModel);
}
[HttpPut("/api/multi-collections/{id:int}")]
[Tags("Multi Collections")]
[EndpointSummary("Update a multi collection")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(MultiCollectionResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Update(
int id,
[Required] [FromBody] UpdateMultiCollectionRequest request,
CancellationToken cancellationToken)
{
Either<BaseError, Unit> result = await mediator.Send(request.ToCommand(id), cancellationToken);
return await result.Match(
Left: error => Task.FromResult(error.ToErrorResult()),
Right: async _ =>
{
Option<MultiCollectionViewModel> multiCollection =
await mediator.Send(new GetMultiCollectionById(id), cancellationToken);
return multiCollection.Match(
Some: vm => (IActionResult)new OkObjectResult(ProjectToResponseModel(vm)),
None: () => ApiResults.NotFoundProblem());
});
}
[HttpDelete("/api/multi-collections/{id:int}")]
[Tags("Multi Collections")]
[EndpointSummary("Delete a multi collection")]
[EndpointGroupName("general")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
{
Either<BaseError, Unit> result = await mediator.Send(new DeleteMultiCollection(id), cancellationToken);
return result.ToDeletedResult();
}
private static MultiCollectionResponseModel ProjectToResponseModel(MultiCollectionViewModel vm)
{
List<MultiCollectionItemResponseModel> items = vm.Items
.Map(i => new MultiCollectionItemResponseModel(
i.Collection.Id,
null,
i.Collection.Name,
i.ScheduleAsGroup,
i.PlaybackOrder))
.Concat(vm.SmartItems.Map(i => new MultiCollectionItemResponseModel(
null,
i.SmartCollection.Id,
i.SmartCollection.Name,
i.ScheduleAsGroup,
i.PlaybackOrder)))
.ToList();
return new MultiCollectionResponseModel(vm.Id, vm.Name, items);
}
}
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Controllers.Api.Requests;
public record MultiCollectionItemRequest(
int? CollectionId,
int? SmartCollectionId,
bool ScheduleAsGroup,
PlaybackOrder PlaybackOrder);
public record CreateMultiCollectionRequest(string Name, List<MultiCollectionItemRequest> Items)
{
public CreateMultiCollection ToCommand() =>
new(
Name,
(Items ?? new List<MultiCollectionItemRequest>())
.Select(i => new CreateMultiCollectionItem(
i.CollectionId,
i.SmartCollectionId,
i.ScheduleAsGroup,
i.PlaybackOrder))
.ToList());
}
@@ -0,0 +1,32 @@
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Controllers.Api.Requests;
public record CreateRerunCollectionRequest(
string Name,
CollectionType CollectionType,
int SelectedId,
PlaybackOrder FirstRunPlaybackOrder,
PlaybackOrder RerunPlaybackOrder)
{
public CreateRerunCollection ToCommand()
{
(MediaCollectionViewModel collection,
MultiCollectionViewModel multiCollection,
SmartCollectionViewModel smartCollection,
NamedMediaItemViewModel mediaItem) =
RerunCollectionRequestMapping.ResolveSelection(CollectionType, SelectedId);
return new CreateRerunCollection(
Name,
CollectionType,
collection,
multiCollection,
smartCollection,
mediaItem,
FirstRunPlaybackOrder,
RerunPlaybackOrder);
}
}
@@ -0,0 +1,76 @@
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Controllers.Api.Requests;
// A rerun collection is a tagged union: CollectionType selects which of Collection /
// MultiCollection / SmartCollection / MediaItem is populated. The Create/Update handlers
// read ONLY the `.Id` (or `.MediaItemId`) off the chosen VM — verified in
// CreateRerunCollectionHandler.Validate / UpdateRerunCollectionHandler.ApplyUpdateRequest,
// both of which do `request.Collection?.Id`, `request.MultiCollection?.Id`,
// `request.SmartCollection?.Id`, `request.MediaItem?.MediaItemId` and nothing else. So we
// construct a minimal VM carrying just the selected id (defaults elsewhere) rather than
// doing extra lookup queries. The remaining VMs are left null (the handler expects that).
internal static class RerunCollectionRequestMapping
{
/// <summary>
/// The only <see cref="CollectionType" /> values valid as a rerun collection's selected
/// source. Excludes Playlist / RerunFirstRun / RerunRerun / SearchQuery / Fake* — those
/// are not media-item-backed collection types and must not be resolved via
/// <see cref="ResolveSelection" />. Callers (the controller) must check this first.
/// </summary>
public static bool IsSupportedSelectionType(CollectionType collectionType) =>
collectionType is CollectionType.Collection
or CollectionType.MultiCollection
or CollectionType.SmartCollection
or CollectionType.TelevisionShow
or CollectionType.TelevisionSeason
or CollectionType.Artist
or CollectionType.Movie
or CollectionType.Episode
or CollectionType.MusicVideo
or CollectionType.OtherVideo
or CollectionType.Song
or CollectionType.Image
or CollectionType.RemoteStream;
public static (MediaCollectionViewModel Collection,
MultiCollectionViewModel MultiCollection,
SmartCollectionViewModel SmartCollection,
NamedMediaItemViewModel MediaItem) ResolveSelection(CollectionType collectionType, int selectedId) =>
collectionType switch
{
CollectionType.Collection => (
new MediaCollectionViewModel(
CollectionType.Collection,
selectedId,
string.Empty,
false,
MediaItemState.Normal),
null,
null,
null),
CollectionType.MultiCollection => (
null,
new MultiCollectionViewModel(selectedId, string.Empty, [], []),
null,
null),
CollectionType.SmartCollection => (
null,
null,
new SmartCollectionViewModel(selectedId, string.Empty, string.Empty),
null),
// Any media-item-backed collection type (TelevisionShow, TelevisionSeason,
// Artist, Movie, Episode, MusicVideo, OtherVideo, Song, Image, ...) resolves to
// the MediaItem VM keyed by media item id. The caller (RerunCollectionController)
// has already validated CollectionType via IsSupportedSelectionType, so this
// catch-all only ever receives a real media-item type.
_ => (
null,
null,
null,
new NamedMediaItemViewModel(selectedId, string.Empty))
};
}
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Application.MediaCollections;
namespace ErsatzTV.Controllers.Api.Requests;
public record UpdateMultiCollectionRequest(string Name, List<MultiCollectionItemRequest> Items)
{
public UpdateMultiCollection ToCommand(int id) =>
new(
id,
Name,
(Items ?? new List<MultiCollectionItemRequest>())
.Select(i => new UpdateMultiCollectionItem(
i.CollectionId,
i.SmartCollectionId,
i.ScheduleAsGroup,
i.PlaybackOrder))
.ToList());
}
@@ -0,0 +1,33 @@
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Controllers.Api.Requests;
public record UpdateRerunCollectionRequest(
string Name,
CollectionType CollectionType,
int SelectedId,
PlaybackOrder FirstRunPlaybackOrder,
PlaybackOrder RerunPlaybackOrder)
{
public UpdateRerunCollection ToCommand(int id)
{
(MediaCollectionViewModel collection,
MultiCollectionViewModel multiCollection,
SmartCollectionViewModel smartCollection,
NamedMediaItemViewModel mediaItem) =
RerunCollectionRequestMapping.ResolveSelection(CollectionType, SelectedId);
return new UpdateRerunCollection(
id,
Name,
CollectionType,
collection,
multiCollection,
smartCollection,
mediaItem,
FirstRunPlaybackOrder,
RerunPlaybackOrder);
}
}
@@ -0,0 +1,148 @@
using System.ComponentModel.DataAnnotations;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.MediaCollections;
using ErsatzTV.Extensions;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers.Api;
[ApiController]
public class RerunCollectionController(IMediator mediator) : ControllerBase
{
private const int MaxPageSize = 100;
[HttpGet("/api/rerun-collections", Name = "GetRerunCollections")]
[Tags("Rerun Collections")]
[EndpointSummary("Get all rerun collections (paged)")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(PagedRerunCollectionsResponseModel), StatusCodes.Status200OK)]
public async Task<PagedRerunCollectionsResponseModel> GetAll(
[FromQuery] string query = "",
[FromQuery] int pageNum = 0,
[FromQuery] int pageSize = 100,
CancellationToken cancellationToken = default)
{
int clampedPageNum = Math.Max(0, pageNum);
int clampedPageSize = Math.Clamp(pageSize, 1, MaxPageSize);
PagedRerunCollectionsViewModel result = await mediator.Send(
new GetPagedRerunCollections(query ?? string.Empty, clampedPageNum, clampedPageSize),
cancellationToken);
return new PagedRerunCollectionsResponseModel(
result.TotalCount,
result.Page.Map(ProjectToResponseModel).ToList());
}
[HttpGet("/api/rerun-collections/{id:int}", Name = "GetRerunCollectionById")]
[Tags("Rerun Collections")]
[EndpointSummary("Get a rerun collection by id")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(RerunCollectionResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
{
Option<RerunCollectionViewModel> result =
await mediator.Send(new GetRerunCollectionById(id), cancellationToken);
return result.Map(ProjectToResponseModel).ToGetResult();
}
[HttpPost("/api/rerun-collections")]
[Tags("Rerun Collections")]
[EndpointSummary("Create a rerun collection")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(RerunCollectionResponseModel), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Create(
[Required] [FromBody] CreateRerunCollectionRequest request,
CancellationToken cancellationToken)
{
if (!RerunCollectionRequestMapping.IsSupportedSelectionType(request.CollectionType))
{
return BaseError.New(
$"Unsupported collection type '{request.CollectionType}' for a rerun collection")
.ToErrorResult();
}
Either<BaseError, RerunCollectionViewModel> result =
await mediator.Send(request.ToCommand(), cancellationToken);
return result.ToCreatedResult(
vm => $"/api/rerun-collections/{vm.Id}",
ProjectToResponseModel);
}
[HttpPut("/api/rerun-collections/{id:int}")]
[Tags("Rerun Collections")]
[EndpointSummary("Update a rerun collection")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(RerunCollectionResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Update(
int id,
[Required] [FromBody] UpdateRerunCollectionRequest request,
CancellationToken cancellationToken)
{
if (!RerunCollectionRequestMapping.IsSupportedSelectionType(request.CollectionType))
{
return BaseError.New(
$"Unsupported collection type '{request.CollectionType}' for a rerun collection")
.ToErrorResult();
}
Either<BaseError, Unit> result = await mediator.Send(request.ToCommand(id), cancellationToken);
return await result.Match(
Left: error => Task.FromResult(error.ToErrorResult()),
Right: async _ =>
{
Option<RerunCollectionViewModel> rerunCollection =
await mediator.Send(new GetRerunCollectionById(id), cancellationToken);
return rerunCollection.Match(
Some: vm => (IActionResult)new OkObjectResult(ProjectToResponseModel(vm)),
None: () => ApiResults.NotFoundProblem());
});
}
[HttpDelete("/api/rerun-collections/{id:int}")]
[Tags("Rerun Collections")]
[EndpointSummary("Delete a rerun collection")]
[EndpointGroupName("general")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
{
Either<BaseError, Unit> result = await mediator.Send(new DeleteRerunCollection(id), cancellationToken);
return result.ToDeletedResult();
}
// A rerun collection is a tagged union keyed by CollectionType; exactly one of
// Collection / MultiCollection / SmartCollection / MediaItem is populated on the VM.
// Flatten it to a single selected id + name for the SPA.
private static RerunCollectionResponseModel ProjectToResponseModel(RerunCollectionViewModel vm)
{
int? selectedId = vm.Collection?.Id
?? vm.MultiCollection?.Id
?? vm.SmartCollection?.Id
?? vm.MediaItem?.MediaItemId;
string selectedName = vm.Collection?.Name
?? vm.MultiCollection?.Name
?? vm.SmartCollection?.Name
?? vm.MediaItem?.Name;
return new RerunCollectionResponseModel(
vm.Id,
vm.Name,
vm.CollectionType,
selectedId,
selectedName,
vm.FirstRunPlaybackOrder,
vm.RerunPlaybackOrder);
}
}
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -104,8 +104,8 @@ been added to the redirect map yet.
| Blazor route | File | Blocking issue |
|---|---|---|
| `/media/multi-collections`(`/add`, `/{Id}/edit`) | `MultiCollections.razor`, `MultiCollectionEditor.razor` | #151 (multi-collection management API) |
| `/media/rerun-collections`(`/add`, `/{Id}/edit`) | `RerunCollections.razor`, `RerunCollectionEditor.razor` | #152 (rerun-collection management API) |
| `/media/multi-collections`(`/add`, `/{Id}/edit`) | `MultiCollections.razor`, `MultiCollectionEditor.razor` | #151 **REST API DONE** (`MultiCollectionController`, `/api/multi-collections` CRUD + `web/src/api/multiCollections.ts`); SPA editor screen still pending (follow-up) |
| `/media/rerun-collections`(`/add`, `/{Id}/edit`) | `RerunCollections.razor`, `RerunCollectionEditor.razor` | #152 **REST API DONE** (`RerunCollectionController`, `/api/rerun-collections` CRUD + `web/src/api/rerunCollections.ts`); SPA editor screen still pending (follow-up) |
| `/media/playlists`(`/{Id}`) editing depth beyond what `/app/collections` covers | `Playlists.razor`, `PlaylistEditor.razor` | #153 (playlist variant management API) |
**#155 RESOLVED** (collection-items enumeration): `GET /api/collections/{id}/items` (paged) now returns a
+21 -1
View File
@@ -2,7 +2,7 @@
*Generated by `scripts/generate-endpoint-index.py` from `ErsatzTV/wwwroot/openapi/v1.json`. Do not edit by hand -- regenerated by `scripts/update-openapi.sh`.*
114 endpoints, 176 operations.
118 endpoints, 186 operations.
## Artists
@@ -188,6 +188,16 @@
|---|---|---|---|
| GET | `/api/movies/{id}` | GetMovieById | Get a movie by id |
## Multi Collections
| Method | Path | Operation | Summary |
|---|---|---|---|
| GET | `/api/multi-collections` | GetMultiCollections | Get all multi collections (paged) |
| POST | `/api/multi-collections` | | Create a multi collection |
| DELETE | `/api/multi-collections/{id}` | | Delete a multi collection |
| GET | `/api/multi-collections/{id}` | GetMultiCollectionById | Get a multi collection by id |
| PUT | `/api/multi-collections/{id}` | | Update a multi collection |
## Playlists
| Method | Path | Operation | Summary |
@@ -216,6 +226,16 @@
| GET | `/api/playouts/{id}/templates` | GetPlayoutTemplates | Get a block playout's templates |
| PUT | `/api/playouts/{id}/templates` | | Replace a block playout's templates |
## Rerun Collections
| Method | Path | Operation | Summary |
|---|---|---|---|
| GET | `/api/rerun-collections` | GetRerunCollections | Get all rerun collections (paged) |
| POST | `/api/rerun-collections` | | Create a rerun collection |
| DELETE | `/api/rerun-collections/{id}` | | Delete a rerun collection |
| GET | `/api/rerun-collections/{id}` | GetRerunCollectionById | Get a rerun collection by id |
| PUT | `/api/rerun-collections/{id}` | | Update a rerun collection |
## Resolution
| Method | Path | Operation | Summary |
+57
View File
@@ -432,12 +432,23 @@ export interface components {
"playlistId": null | number;
"expression": null | string;
"useChaptersAsMediaItems": boolean;
};
"CreateMultiCollectionRequest": {
"name": null | string;
"items": null | Array<components["schemas"]["MultiCollectionItemRequest"]>;
};
"CreatePlayoutRequest": {
"channelId": number;
"scheduleKind": components["schemas"]["PlayoutScheduleKind"];
"programScheduleId": null | number;
"scheduleFile": null | string;
};
"CreateRerunCollectionRequest": {
"name": null | string;
"collectionType": components["schemas"]["CollectionType"];
"selectedId": number;
"firstRunPlaybackOrder": components["schemas"]["PlaybackOrder"];
"rerunPlaybackOrder": components["schemas"]["PlaybackOrder"];
};
"CreateResolutionRequest": {
"width": number;
@@ -846,12 +857,30 @@ export interface components {
"state": components["schemas"]["MediaItemState"];
"poster": string;
"fanArt": string;
};
"MultiCollectionItemRequest": {
"collectionId": null | number;
"smartCollectionId": null | number;
"scheduleAsGroup": boolean;
"playbackOrder": components["schemas"]["PlaybackOrder"];
};
"MultiCollectionItemResponseModel": {
"collectionId": null | number;
"smartCollectionId": null | number;
"name": string;
"scheduleAsGroup": boolean;
"playbackOrder": components["schemas"]["PlaybackOrder"];
};
"MultiCollectionItemViewModel": {
"multiCollectionId": number;
"collection": components["schemas"]["MediaCollectionViewModel"];
"scheduleAsGroup": boolean;
"playbackOrder": components["schemas"]["PlaybackOrder"];
};
"MultiCollectionResponseModel": {
"id": number;
"name": string;
"items": Array<components["schemas"]["MultiCollectionItemResponseModel"]>;
};
"MultiCollectionSmartItemViewModel": {
"multiCollectionId": number;
@@ -879,6 +908,10 @@ export interface components {
"PagedLogEntriesResponseModel": {
"totalCount": number;
"page": Array<components["schemas"]["LogEntryResponseModel"]>;
};
"PagedMultiCollectionsResponseModel": {
"totalCount": number;
"page": Array<components["schemas"]["MultiCollectionResponseModel"]>;
};
"PagedPlayoutHistoryResponseModel": {
"totalCount": number;
@@ -891,6 +924,10 @@ export interface components {
"PagedPlayoutsResponseModel": {
"totalCount": number;
"page": null | Array<components["schemas"]["PlayoutListItemResponseModel"]>;
};
"PagedRerunCollectionsResponseModel": {
"totalCount": number;
"page": Array<components["schemas"]["RerunCollectionResponseModel"]>;
};
"PagedTraktListsResponseModel": {
"totalCount": number;
@@ -1139,6 +1176,15 @@ export interface components {
"ReplaceTemplateRequest": {
"name": null | string;
"items": null | Array<components["schemas"]["TemplateItemRequest"]>;
};
"RerunCollectionResponseModel": {
"id": number;
"name": string;
"collectionType": components["schemas"]["CollectionType"];
"selectedId": null | number;
"selectedName": null | string;
"firstRunPlaybackOrder": components["schemas"]["PlaybackOrder"];
"rerunPlaybackOrder": components["schemas"]["PlaybackOrder"];
};
"RerunCollectionViewModel": {
"id": number;
@@ -1468,6 +1514,10 @@ export interface components {
"searchingMinimumLogLevel": components["schemas"]["LogEventLevel"];
"streamingMinimumLogLevel": components["schemas"]["LogEventLevel"];
"httpMinimumLogLevel": components["schemas"]["LogEventLevel"];
};
"UpdateMultiCollectionRequest": {
"name": null | string;
"items": null | Array<components["schemas"]["MultiCollectionItemRequest"]>;
};
"UpdatePlayoutDetailsRequest": {
"dailyRebuildTime": null | string;
@@ -1477,6 +1527,13 @@ export interface components {
"daysToBuild": number;
"skipMissingItems": boolean;
"scriptedScheduleTimeoutSeconds": number;
};
"UpdateRerunCollectionRequest": {
"name": null | string;
"collectionType": components["schemas"]["CollectionType"];
"selectedId": number;
"firstRunPlaybackOrder": components["schemas"]["PlaybackOrder"];
"rerunPlaybackOrder": components["schemas"]["PlaybackOrder"];
};
"UpdateScannerSettingsRequest": {
"libraryRefreshInterval": number;
+50
View File
@@ -0,0 +1,50 @@
import { request } from './client';
import type { components } from './generated/v1';
export type MultiCollection = components['schemas']['MultiCollectionResponseModel'];
export type MultiCollectionItem = components['schemas']['MultiCollectionItemResponseModel'];
export type PagedMultiCollections = components['schemas']['PagedMultiCollectionsResponseModel'];
export type CreateMultiCollectionRequest = components['schemas']['CreateMultiCollectionRequest'];
export type UpdateMultiCollectionRequest = components['schemas']['UpdateMultiCollectionRequest'];
export interface GetMultiCollectionsParams {
query?: string;
pageNum?: number;
pageSize?: number;
}
export function getMultiCollections(params: GetMultiCollectionsParams = {}): Promise<PagedMultiCollections> {
const searchParams = new URLSearchParams();
if (params.query != null) {
searchParams.set('query', params.query);
}
if (params.pageNum != null) {
searchParams.set('pageNum', String(params.pageNum));
}
if (params.pageSize != null) {
searchParams.set('pageSize', String(params.pageSize));
}
const queryString = searchParams.toString();
return request<PagedMultiCollections>(`/api/multi-collections${queryString ? `?${queryString}` : ''}`);
}
export function getMultiCollection(id: number): Promise<MultiCollection> {
return request<MultiCollection>(`/api/multi-collections/${id}`);
}
export function createMultiCollection(body: CreateMultiCollectionRequest): Promise<MultiCollection> {
return request<MultiCollection>('/api/multi-collections', { body, method: 'POST' });
}
export function updateMultiCollection(id: number, body: UpdateMultiCollectionRequest): Promise<MultiCollection> {
return request<MultiCollection>(`/api/multi-collections/${id}`, { body, method: 'PUT' });
}
export function deleteMultiCollection(id: number): Promise<void> {
return request<void>(`/api/multi-collections/${id}`, { method: 'DELETE' });
}
+49
View File
@@ -0,0 +1,49 @@
import { request } from './client';
import type { components } from './generated/v1';
export type RerunCollection = components['schemas']['RerunCollectionResponseModel'];
export type PagedRerunCollections = components['schemas']['PagedRerunCollectionsResponseModel'];
export type CreateRerunCollectionRequest = components['schemas']['CreateRerunCollectionRequest'];
export type UpdateRerunCollectionRequest = components['schemas']['UpdateRerunCollectionRequest'];
export interface GetRerunCollectionsParams {
query?: string;
pageNum?: number;
pageSize?: number;
}
export function getRerunCollections(params: GetRerunCollectionsParams = {}): Promise<PagedRerunCollections> {
const searchParams = new URLSearchParams();
if (params.query != null) {
searchParams.set('query', params.query);
}
if (params.pageNum != null) {
searchParams.set('pageNum', String(params.pageNum));
}
if (params.pageSize != null) {
searchParams.set('pageSize', String(params.pageSize));
}
const queryString = searchParams.toString();
return request<PagedRerunCollections>(`/api/rerun-collections${queryString ? `?${queryString}` : ''}`);
}
export function getRerunCollection(id: number): Promise<RerunCollection> {
return request<RerunCollection>(`/api/rerun-collections/${id}`);
}
export function createRerunCollection(body: CreateRerunCollectionRequest): Promise<RerunCollection> {
return request<RerunCollection>('/api/rerun-collections', { body, method: 'POST' });
}
export function updateRerunCollection(id: number, body: UpdateRerunCollectionRequest): Promise<RerunCollection> {
return request<RerunCollection>(`/api/rerun-collections/${id}`, { body, method: 'PUT' });
}
export function deleteRerunCollection(id: number): Promise<void> {
return request<void>(`/api/rerun-collections/${id}`, { method: 'DELETE' });
}