diff --git a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs index a4b05f5..50483a2 100644 --- a/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/GetCollectionTests.cs @@ -140,7 +140,7 @@ public async Task Get_RootFlat_ReturnsEntryPointFlat_WhenAuthAndHeader() // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); - collection!.Id.Should().Be("http://localhost/1/collections/RootStorage"); + collection!.Id.Should().Be("http://localhost/1/collections/root"); collection.PublicId.Should().Be("http://localhost/1"); collection.Items!.Count.Should().Be(2); collection.Items[0].Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); @@ -162,13 +162,14 @@ public async Task Get_RootFlat_ReturnsEntryPointFlat_WhenCalledById() // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); - collection!.Id.Should().Be("http://localhost/1/collections/RootStorage"); + collection!.Id.Should().Be("http://localhost/1/collections/root"); collection.PublicId.Should().Be("http://localhost/1"); collection.Items!.Count.Should().Be(2); collection.Items[0].Id.Should().Be("http://localhost/1/collections/FirstChildCollection"); collection.TotalItems.Should().Be(2); collection.CreatedBy.Should().Be("admin"); collection.Behavior.Should().Contain("public-iiif"); + collection.Parent.Should().BeNull(); } [Fact] @@ -192,6 +193,7 @@ public async Task Get_ChildFlat_ReturnsEntryPointFlat_WhenCalledByChildId() collection.TotalItems.Should().Be(1); collection.CreatedBy.Should().Be("admin"); collection.Behavior.Should().Contain("public-iiif"); + collection.Parent.Should().Be("http://localhost/1/collections/root"); } [Fact] @@ -214,6 +216,7 @@ public async Task Get_PrivateChild_ReturnsCorrectlyFlatAndHierarchical() flatCollection.CreatedBy.Should().Be("admin"); flatCollection.Behavior.Should().Contain("storage-collection"); flatCollection.Behavior.Should().NotContain("public-iiif"); + flatCollection.Parent.Should().Be("http://localhost/1/collections/root"); hierarchicalResponse.StatusCode.Should().Be(HttpStatusCode.NotFound); } @@ -238,7 +241,7 @@ public async Task Get_RootFlat_ReturnsItems_WhenCalledWithPageSize() } [Fact] - public async Task Get_RootFlat_ReturnsReducedItems_WhenCalledWithSmallPageSize() + public async Task Get_ChildFlat_ReturnsReducedItems_WhenCalledWithSmallPageSize() { // Arrange var requestMessage = @@ -323,7 +326,7 @@ public async Task Get_RootFlat_ReturnsMaxPageSize_WhenCalledPageSizeExceedsMax() [InlineData("id")] [InlineData("slug")] [InlineData("created")] - public async Task Get_RootFlat_ReturnsCorrectItem_WhenCalledWithSmallPageSizeAndOrderBy(string field) + public async Task Get_ChildFlat_ReturnsCorrectItem_WhenCalledWithSmallPageSizeAndOrderBy(string field) { // Arrange var requestMessage = @@ -363,7 +366,7 @@ public async Task Get_RootFlat_ReturnsFirstPageWithSecondItem_WhenCalledWithSmal // Assert collection.TotalItems.Should().Be(2); collection.View!.PageSize.Should().Be(1); - collection.View.Id.Should().Be($"http://localhost/1/collections/RootStorage?page=1&pageSize=1&orderByDescending={field}"); + collection.View.Id.Should().Be($"http://localhost/1/collections/root?page=1&pageSize=1&orderByDescending={field}"); collection.View.Page.Should().Be(1); collection.View.TotalPages.Should().Be(2); collection.Items!.Count.Should().Be(1); @@ -371,7 +374,7 @@ public async Task Get_RootFlat_ReturnsFirstPageWithSecondItem_WhenCalledWithSmal } [Fact] - public async Task Get_RootFlat_IgnoresOrderBy_WhenCalledWithInvalidOrderBy() + public async Task Get_ChildFlat_IgnoresOrderBy_WhenCalledWithInvalidOrderBy() { // Arrange var requestMessage = @@ -386,7 +389,7 @@ public async Task Get_RootFlat_IgnoresOrderBy_WhenCalledWithInvalidOrderBy() // Assert collection.TotalItems.Should().Be(2); collection.View!.PageSize.Should().Be(1); - collection.View.Id.Should().Be($"http://localhost/1/collections/RootStorage?page=1&pageSize=1"); + collection.View.Id.Should().Be($"http://localhost/1/collections/root?page=1&pageSize=1"); collection.View.Page.Should().Be(1); collection.View.TotalPages.Should().Be(2); collection.Items!.Count.Should().Be(1); diff --git a/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs index dac7f05..4f3f8c8 100644 --- a/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs @@ -79,6 +79,9 @@ public async Task CreateCollection_CreatesCollection_WhenAllValuesProvided() fromDatabase.Tags.Should().Be("some, tags"); fromDatabase.IsPublic.Should().BeTrue(); fromDatabase.IsStorageCollection.Should().BeTrue(); + responseCollection!.View!.PageSize.Should().Be(20); + responseCollection.View.Page.Should().Be(1); + responseCollection.View.Id.Should().Contain("?page=1&pageSize=20"); } [Fact] @@ -283,6 +286,9 @@ public async Task UpdateCollection_UpdatesCollection_WhenAllValuesProvided() fromDatabase.Tags.Should().Be("some, tags, 2"); fromDatabase.IsPublic.Should().BeTrue(); fromDatabase.IsStorageCollection.Should().BeTrue(); + responseCollection!.View!.PageSize.Should().Be(20); + responseCollection.View.Page.Should().Be(1); + responseCollection.View.Id.Should().Contain("?page=1&pageSize=20"); } [Fact] diff --git a/src/IIIFPresentation/API/Converters/CollectionConverter.cs b/src/IIIFPresentation/API/Converters/CollectionConverter.cs index 146d937..8ce5370 100644 --- a/src/IIIFPresentation/API/Converters/CollectionConverter.cs +++ b/src/IIIFPresentation/API/Converters/CollectionConverter.cs @@ -120,7 +120,7 @@ private static View GenerateView(Models.Database.Collections.Collection dbAsset, TotalPages = totalPages, }; - if (totalPages > 1) + if (currentPage > 1) { view.First = dbAsset.GenerateFlatCollectionViewFirst(urlRoots, pageSize, orderQueryParam); view.Previous = dbAsset.GenerateFlatCollectionViewPrevious(urlRoots, currentPage, pageSize, orderQueryParam); diff --git a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs index c0a43b0..80b520b 100644 --- a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs +++ b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs @@ -2,6 +2,7 @@ using Core; using Microsoft.EntityFrameworkCore; using Models.API.Collection; +using Models.Database.Collections; using Repository; using Repository.Helpers; @@ -35,4 +36,24 @@ public static class PresentationContextX return null; } + + public static async Task RetrieveCollection(this PresentationContext dbContext, int customerId, string collectionId, + string rootCollectionId, CancellationToken cancellationToken) + { + Collection? collection; + if (collectionId.Equals(rootCollectionId, StringComparison.OrdinalIgnoreCase)) + { + collection = await dbContext.Collections.AsNoTracking().FirstOrDefaultAsync( + s => s.CustomerId == customerId && s.Parent == null, + cancellationToken); + } + else + { + collection = await dbContext.Collections.AsNoTracking().FirstOrDefaultAsync( + s => s.CustomerId == customerId && s.Id == collectionId, + cancellationToken); + } + + return collection; + } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Helpers/RootCollection.cs b/src/IIIFPresentation/API/Features/Storage/Helpers/RootCollection.cs new file mode 100644 index 0000000..a9147a3 --- /dev/null +++ b/src/IIIFPresentation/API/Features/Storage/Helpers/RootCollection.cs @@ -0,0 +1,6 @@ +namespace API.Features.Storage.Helpers; + +public static class RootCollection +{ + public const string Id = "root"; +} \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs index fc18aca..21f065f 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -1,6 +1,7 @@ using API.Auth; using API.Converters; using API.Features.Storage.Helpers; +using API.Helpers; using API.Infrastructure.Requests; using API.Settings; using Core; @@ -33,8 +34,20 @@ public class CreateCollectionHandler( { private readonly ApiSettings settings = options.Value; + private const int CurrentPage = 1; + public async Task> Handle(CreateCollection request, CancellationToken cancellationToken) { + // check parent exists + var parentCollection = await dbContext.RetrieveCollection(request.CustomerId, + request.Collection.Parent.GetLastPathElement(), RootCollection.Id, cancellationToken); + + if (parentCollection == null) + { + return ModifyEntityResult.Failure( + $"The parent collection could not be found", WriteResult.Conflict); + } + var collection = new Collection() { Id = Guid.NewGuid().ToString(), @@ -45,7 +58,7 @@ public async Task> Handle(CreateCollection re IsPublic = request.Collection.Behavior.IsPublic(), IsStorageCollection = request.Collection.Behavior.IsStorageCollection(), Label = request.Collection.Label, - Parent = request.Collection.Parent!.GetLastPathElement(), + Parent = parentCollection.Id, Slug = request.Collection.Slug, Thumbnail = request.Collection.Thumbnail, Tags = request.Collection.Tags, @@ -66,8 +79,10 @@ public async Task> Handle(CreateCollection re collection.FullPath = CollectionRetrieval.RetrieveFullPathForCollection(collection, dbContext); } + collection.UpdateParentForRootIfRequired(request.Collection.Parent); + return ModifyEntityResult.Success( - collection.ToFlatCollection(request.UrlRoots, 1, settings.PageSize, 0, []), // there can be no items attached to this, as it's just been created + collection.ToFlatCollection(request.UrlRoots, settings.PageSize, CurrentPage, 0, []), // there can be no items attached to this, as it's just been created WriteResult.Created); } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs index 6f697ef..750284b 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs @@ -1,4 +1,5 @@ -using API.Features.Storage.Models; +using API.Features.Storage.Helpers; +using API.Features.Storage.Models; using API.Helpers; using MediatR; using Microsoft.EntityFrameworkCore; @@ -28,25 +29,11 @@ public class GetCollection( public class GetCollectionHandler(PresentationContext dbContext) : IRequestHandler { - private const string RootCollection = "root"; - public async Task Handle(GetCollection request, CancellationToken cancellationToken) { - Collection? collection; - - if (request.Id.Equals(RootCollection, StringComparison.OrdinalIgnoreCase)) - { - collection = await dbContext.Collections.AsNoTracking().FirstOrDefaultAsync( - s => s.CustomerId == request.CustomerId && s.Parent == null, - cancellationToken); - } - else - { - collection = await dbContext.Collections.AsNoTracking().FirstOrDefaultAsync( - s => s.CustomerId == request.CustomerId && s.Id == request.Id, - cancellationToken); - } + Collection? collection = await dbContext.RetrieveCollection(request.CustomerId, request.Id, RootCollection.Id, + cancellationToken); List? items = null; int total = 0; @@ -71,6 +58,17 @@ public async Task Handle(GetCollection request, if (collection.Parent != null) { collection.FullPath = CollectionRetrieval.RetrieveFullPathForCollection(collection, dbContext); + + var parentCollection = await dbContext.RetrieveCollection(request.CustomerId, collection.Parent, RootCollection.Id, + cancellationToken); + if (parentCollection!.Parent == null) + { + collection.Parent = RootCollection.Id; + } + } + else + { + collection.Id = RootCollection.Id; } } diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs index d9edd77..a6ee411 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/UpdateCollection.cs @@ -1,6 +1,7 @@ using API.Auth; using API.Converters; using API.Features.Storage.Helpers; +using API.Helpers; using API.Infrastructure.Requests; using API.Settings; using Core; @@ -47,13 +48,22 @@ public async Task> Handle(UpdateCollection re return ModifyEntityResult.Failure( "Could not find a matching record for the provided collection id", WriteResult.NotFound); } + + var parentCollection = await dbContext.RetrieveCollection(request.CustomerId, + request.Collection.Parent.GetLastPathElement(), RootCollection.Id, cancellationToken); + + if (parentCollection == null) + { + return ModifyEntityResult.Failure( + $"The parent collection could not be found", WriteResult.Conflict); + } collectionFromDatabase.Modified = DateTime.UtcNow; collectionFromDatabase.ModifiedBy = Authorizer.GetUser(); collectionFromDatabase.IsPublic = request.Collection.Behavior.IsPublic(); collectionFromDatabase.IsStorageCollection = request.Collection.Behavior.IsStorageCollection(); collectionFromDatabase.Label = request.Collection.Label; - collectionFromDatabase.Parent = request.Collection.Parent.GetLastPathElement(); + collectionFromDatabase.Parent = parentCollection.Id; collectionFromDatabase.Slug = request.Collection.Slug; collectionFromDatabase.Thumbnail = request.Collection.Thumbnail; collectionFromDatabase.Tags = request.Collection.Tags; @@ -85,6 +95,8 @@ public async Task> Handle(UpdateCollection re CollectionRetrieval.RetrieveFullPathForCollection(collectionFromDatabase, dbContext); } + collectionFromDatabase.UpdateParentForRootIfRequired(request.Collection.Parent); + return ModifyEntityResult.Success( collectionFromDatabase.ToFlatCollection(request.UrlRoots, settings.PageSize, DefaultCurrentPage, total, await items.ToListAsync(cancellationToken: cancellationToken))); diff --git a/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs b/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs index 22e669b..8a70f8a 100644 --- a/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs +++ b/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs @@ -1,4 +1,6 @@ using API.Converters; +using API.Features.Storage.Helpers; +using Core.Helpers; using Models.Database.Collections; namespace API.Helpers; @@ -43,4 +45,15 @@ public static Uri GenerateFlatCollectionViewLast(this Collection collection, Url public static string GenerateFullPath(this Collection collection, string itemSlug) => $"{(collection.Parent != null ? $"{collection.Slug}/" : string.Empty)}{itemSlug}"; + + public static Collection UpdateParentForRootIfRequired(this Collection collection, string parentToChangeTo) + { + // everything saved so set the response value to be the root collection if required + if (parentToChangeTo.GetLastPathElement() == RootCollection.Id) + { + collection.Parent = RootCollection.Id; + } + + return collection; + } }