From 8d33a2b66e8de775c8407b8422fa7765de65f44d Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 9 Oct 2024 09:47:08 +0100 Subject: [PATCH 01/11] push up wip for hierarchy table --- .../Models/Database/Collections/Manifest.cs | 6 +++++ .../Models/Database/General/Hierarchy.cs | 26 +++++++++++++++++++ src/IIIFPresentation/Models/Models.csproj | 4 --- .../Repository/PresentationContext.cs | 13 ++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/IIIFPresentation/Models/Database/Collections/Manifest.cs create mode 100644 src/IIIFPresentation/Models/Database/General/Hierarchy.cs diff --git a/src/IIIFPresentation/Models/Database/Collections/Manifest.cs b/src/IIIFPresentation/Models/Database/Collections/Manifest.cs new file mode 100644 index 00000000..fe76c59f --- /dev/null +++ b/src/IIIFPresentation/Models/Database/Collections/Manifest.cs @@ -0,0 +1,6 @@ +namespace Models.Database.Collections; + +public class Manifest +{ + public string Id { get; set; } +} \ No newline at end of file diff --git a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs new file mode 100644 index 00000000..bb4322a2 --- /dev/null +++ b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs @@ -0,0 +1,26 @@ + +using Models.Database.Collections; + +namespace Models.Database.General; + +public class Hierarchy +{ + public int Id { get; set; } + + public string? CollectionId { get; set; } + + public Collection? Collection { get; set; } + + public string? ManifestId { get; set; } + + public Manifest? Manifest { get; set; } + public required string Slug { get; set; } + + public string? Parent { get; set; } + + public int? ItemsOrder { get; set; } + + public bool Public { get; set; } + + public bool Canonical { get; set; } +} \ No newline at end of file diff --git a/src/IIIFPresentation/Models/Models.csproj b/src/IIIFPresentation/Models/Models.csproj index 15464b0e..e43b0df4 100644 --- a/src/IIIFPresentation/Models/Models.csproj +++ b/src/IIIFPresentation/Models/Models.csproj @@ -8,10 +8,6 @@ Library - - - - diff --git a/src/IIIFPresentation/Repository/PresentationContext.cs b/src/IIIFPresentation/Repository/PresentationContext.cs index 3077b713..55e493ed 100644 --- a/src/IIIFPresentation/Repository/PresentationContext.cs +++ b/src/IIIFPresentation/Repository/PresentationContext.cs @@ -1,7 +1,9 @@ using IIIF.Presentation.V3.Strings; using Microsoft.EntityFrameworkCore; using Models.Database.Collections; +using Models.Database.General; using Repository.Converters; +using Manifest = IIIF.Presentation.V2.Manifest; namespace Repository; @@ -18,6 +20,10 @@ public PresentationContext(DbContextOptions options) public virtual DbSet Collections { get; set; } + public virtual DbSet Manifests { get; set; } + + public virtual DbSet Hierarchy { get; set; } + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { configurationBuilder @@ -34,5 +40,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Label).HasColumnType("jsonb"); }); + + modelBuilder.Entity(entity => + { + entity.ToTable(t => + t.HasCheckConstraint("opposite_must_be_null", "collection_id is null or manifest_id is null")); + entity.HasIndex(e => new { e.Slug, e.Parent }); + }); } } \ No newline at end of file From 22d33e01ed39ee3cde0af05ee0946b76ebc6a39a Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Thu, 10 Oct 2024 09:41:16 +0100 Subject: [PATCH 02/11] Add current initial database migration --- .../Models/Database/Collections/Manifest.cs | 2 +- .../Models/Database/General/Hierarchy.cs | 19 +- ...241009163208_addHierarchyTable.Designer.cs | 170 ++++++++++++++++++ .../20241009163208_addHierarchyTable.cs | 61 +++++++ .../PresentationContextModelSnapshot.cs | 63 +++++++ .../Repository/PresentationContext.cs | 5 +- 6 files changed, 310 insertions(+), 10 deletions(-) create mode 100644 src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.Designer.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.cs diff --git a/src/IIIFPresentation/Models/Database/Collections/Manifest.cs b/src/IIIFPresentation/Models/Database/Collections/Manifest.cs index fe76c59f..424c0d67 100644 --- a/src/IIIFPresentation/Models/Database/Collections/Manifest.cs +++ b/src/IIIFPresentation/Models/Database/Collections/Manifest.cs @@ -2,5 +2,5 @@ public class Manifest { - public string Id { get; set; } + public required string Id { get; set; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs index bb4322a2..4f97419b 100644 --- a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs +++ b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs @@ -7,13 +7,10 @@ public class Hierarchy { public int Id { get; set; } - public string? CollectionId { get; set; } + public string? ResourceId { get; set; } - public Collection? Collection { get; set; } + public ResourceType Type { get; set; } - public string? ManifestId { get; set; } - - public Manifest? Manifest { get; set; } public required string Slug { get; set; } public string? Parent { get; set; } @@ -23,4 +20,16 @@ public class Hierarchy public bool Public { get; set; } public bool Canonical { get; set; } + + /// + /// The customer identifier + /// + public int CustomerId { get; set; } +} + +public enum ResourceType +{ + StorageCollection = 0, + IIIFCollection = 1, + Manifest = 2 } \ No newline at end of file diff --git a/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.Designer.cs b/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.Designer.cs new file mode 100644 index 00000000..45f4914c --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.Designer.cs @@ -0,0 +1,170 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Repository; + +#nullable disable + +namespace Repository.Migrations +{ + [DbContext(typeof(PresentationContext))] + [Migration("20241009163208_addHierarchyTable")] + partial class addHierarchyTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Models.Database.Collections.Collection", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("IsStorageCollection") + .HasColumnType("boolean") + .HasColumnName("is_storage_collection"); + + b.Property("ItemsOrder") + .HasColumnType("integer") + .HasColumnName("items_order"); + + b.Property("Label") + .HasColumnType("jsonb") + .HasColumnName("label"); + + b.Property("LockedBy") + .HasColumnType("text") + .HasColumnName("locked_by"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("Parent") + .HasColumnType("text") + .HasColumnName("parent"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.Property("Thumbnail") + .HasColumnType("text") + .HasColumnName("thumbnail"); + + b.Property("UsePath") + .HasColumnType("boolean") + .HasColumnName("use_path"); + + b.HasKey("Id", "CustomerId") + .HasName("pk_collections"); + + b.HasIndex("CustomerId", "Slug", "Parent") + .IsUnique() + .HasDatabaseName("ix_collections_customer_id_slug_parent"); + + b.ToTable("collections", (string)null); + }); + + modelBuilder.Entity("Models.Database.Collections.Manifest", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.HasKey("Id") + .HasName("pk_manifests"); + + b.ToTable("manifests", (string)null); + }); + + modelBuilder.Entity("Models.Database.General.Hierarchy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Canonical") + .HasColumnType("boolean") + .HasColumnName("canonical"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("ItemsOrder") + .HasColumnType("integer") + .HasColumnName("items_order"); + + b.Property("Parent") + .HasColumnType("text") + .HasColumnName("parent"); + + b.Property("Public") + .HasColumnType("boolean") + .HasColumnName("public"); + + b.Property("ResourceId") + .HasColumnType("text") + .HasColumnName("resource_id"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_hierarchy"); + + b.HasIndex("Slug", "Parent", "CustomerId") + .HasDatabaseName("ix_hierarchy_slug_parent_customer_id"); + + b.ToTable("hierarchy", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.cs b/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.cs new file mode 100644 index 00000000..04f2cb60 --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Repository.Migrations +{ + /// + public partial class addHierarchyTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "hierarchy", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + resource_id = table.Column(type: "text", nullable: true), + type = table.Column(type: "integer", nullable: false), + slug = table.Column(type: "text", nullable: false), + parent = table.Column(type: "text", nullable: true), + items_order = table.Column(type: "integer", nullable: true), + @public = table.Column(name: "public", type: "boolean", nullable: false), + canonical = table.Column(type: "boolean", nullable: false), + customer_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_hierarchy", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "manifests", + columns: table => new + { + id = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_manifests", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_hierarchy_slug_parent_customer_id", + table: "hierarchy", + columns: new[] { "slug", "parent", "customer_id" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "hierarchy"); + + migrationBuilder.DropTable( + name: "manifests"); + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs index 28a4f7e2..00938887 100644 --- a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs +++ b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs @@ -98,6 +98,69 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("collections", (string)null); }); + + modelBuilder.Entity("Models.Database.Collections.Manifest", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.HasKey("Id") + .HasName("pk_manifests"); + + b.ToTable("manifests", (string)null); + }); + + modelBuilder.Entity("Models.Database.General.Hierarchy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Canonical") + .HasColumnType("boolean") + .HasColumnName("canonical"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("ItemsOrder") + .HasColumnType("integer") + .HasColumnName("items_order"); + + b.Property("Parent") + .HasColumnType("text") + .HasColumnName("parent"); + + b.Property("Public") + .HasColumnType("boolean") + .HasColumnName("public"); + + b.Property("ResourceId") + .HasColumnType("text") + .HasColumnName("resource_id"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_hierarchy"); + + b.HasIndex("Slug", "Parent", "CustomerId") + .HasDatabaseName("ix_hierarchy_slug_parent_customer_id"); + + b.ToTable("hierarchy", (string)null); + }); #pragma warning restore 612, 618 } } diff --git a/src/IIIFPresentation/Repository/PresentationContext.cs b/src/IIIFPresentation/Repository/PresentationContext.cs index 55e493ed..061f51c4 100644 --- a/src/IIIFPresentation/Repository/PresentationContext.cs +++ b/src/IIIFPresentation/Repository/PresentationContext.cs @@ -3,7 +3,6 @@ using Models.Database.Collections; using Models.Database.General; using Repository.Converters; -using Manifest = IIIF.Presentation.V2.Manifest; namespace Repository; @@ -43,9 +42,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(entity => { - entity.ToTable(t => - t.HasCheckConstraint("opposite_must_be_null", "collection_id is null or manifest_id is null")); - entity.HasIndex(e => new { e.Slug, e.Parent }); + entity.HasIndex(e => new { e.Slug, e.Parent, e.CustomerId }); }); } } \ No newline at end of file From 44226e424ce194f72ed4f1ba6fabf83ce97f7797 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Mon, 14 Oct 2024 09:15:56 +0100 Subject: [PATCH 03/11] getting code to build --- .../Converters/CollectionConverterTests.cs | 99 ++++++++---- .../Helpers/CollectionHelperXTests.cs | 6 +- .../Integration/ModifyCollectionTests.cs | 151 +++++++++++++----- src/IIIFPresentation/API/API.csproj | 3 - .../API/Converters/CollectionConverter.cs | 47 +++--- .../Storage/Helpers/PresentationContextX.cs | 21 +++ .../Storage/Models/CollectionWithItems.cs | 6 +- .../Storage/Models/HierarchicalCollection.cs | 11 ++ .../Storage/Requests/CreateCollection.cs | 28 +++- .../Storage/Requests/DeleteCollection.cs | 21 +-- .../Storage/Requests/GetCollection.cs | 22 +-- .../Requests/GetHierarchicalCollection.cs | 33 ++-- .../Requests/PostHierarchicalCollection.cs | 24 ++- .../Storage/Requests/UpsertCollection.cs | 49 ++++-- .../API/Features/Storage/StorageController.cs | 5 +- .../API/Helpers/CollectionHelperX.cs | 11 +- .../API/Collection/PresentationCollection.cs | 7 +- .../Models/Database/Collections/Collection.cs | 31 ++-- .../Models/Database/Collections/Manifest.cs | 6 - .../Collections/CollectionQueryX.cs | 4 + .../Repository/Helpers/CollectionRetrieval.cs | 21 +-- .../Repository/PresentationContext.cs | 12 +- .../Integration/PresentationContextFixture.cs | 61 ++++++- 23 files changed, 477 insertions(+), 202 deletions(-) create mode 100644 src/IIIFPresentation/API/Features/Storage/Models/HierarchicalCollection.cs delete mode 100644 src/IIIFPresentation/Models/Database/Collections/Manifest.cs diff --git a/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs b/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs index 5f78d170..5ace58cf 100644 --- a/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs +++ b/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs @@ -1,6 +1,9 @@ using API.Converters; +using API.Features.Storage.Models; +using FluentAssertions; using IIIF.Presentation.V3.Strings; using Models.Database.Collections; +using Models.Database.General; #nullable disable @@ -20,7 +23,18 @@ public class CollectionConverterTests public void ToHierarchicalCollection_ConvertsStorageCollection() { // Arrange - var storageRoot = CreateTestStorageRoot(); + var storageRoot = new Collection + { + Id = "some-id", + CustomerId = 1, + Slug = "root", + Label = new LanguageMap + { + { "en", new List { "repository root" } } + }, + Created = DateTime.MinValue, + Modified = DateTime.MinValue + }; // Act var hierarchicalCollection = @@ -54,7 +68,28 @@ public void ToHierarchicalCollection_ConvertsStorageCollectionWithFullPath() public void ToFlatCollection_ConvertsStorageCollection() { // Arrange - var storageRoot = CreateTestStorageRoot(); + var collection = new Collection + { + Id = "some-id", + CustomerId = 1, + Slug = "root", + Label = new LanguageMap + { + { "en", new List { "repository root" } } + }, + Created = DateTime.MinValue, + Modified = DateTime.MinValue + }; + + var hierarchy = new Hierarchy() + { + CollectionId = "some-id", + Slug = "root", + CustomerId = 1, + Type = ResourceType.StorageCollection + }; + + var storageRoot = new HierarchicalCollection(collection, hierarchy); // Act var flatCollection = @@ -83,7 +118,7 @@ public void ToFlatCollection_ConvertsStorageCollection() public void ToFlatCollection_ConvertsStorageCollection_WithFullPath() { // Arrange - var storageRoot = CreateTestCollection(); + var storageRoot = CreateTestHierarchicalCollection(); // Act var flatCollection = @@ -109,16 +144,16 @@ public void ToFlatCollection_ConvertsStorageCollection_WithFullPath() flatCollection.View.First.Should().BeNull(); flatCollection.View.Next.Should().BeNull(); } - + [Fact] public void ToFlatCollection_ConvertsStorageCollection_WithCorrectPaging() { // Arrange - var storageRoot = CreateTestCollection(); + var storageRoot = CreateTestHierarchicalCollection(); // Act var flatCollection = - storageRoot.ToFlatCollection(urlRoots, 1, 2, 3, + storageRoot.ToFlatCollection(urlRoots, 1, 2, 3, new List(CreateTestItems()), "orderBy=created"); // Assert @@ -143,24 +178,6 @@ public void ToFlatCollection_ConvertsStorageCollection_WithCorrectPaging() flatCollection.TotalItems.Should().Be(3); } - private static Collection CreateTestStorageRoot() - { - var storageRoot = new Collection() - { - Id = "some-id", - CustomerId = 1, - Slug = "root", - Label = new LanguageMap - { - { "en", new List { "repository root" } } - }, - Created = DateTime.MinValue, - Modified = DateTime.MinValue - }; - - return storageRoot; - } - private static List CreateTestItems() { var items = new List() @@ -176,7 +193,6 @@ private static List CreateTestItems() }, Created = DateTime.MinValue, Modified = DateTime.MinValue, - Parent = "some-id", FullPath = "top/some-child" } }; @@ -184,9 +200,9 @@ private static List CreateTestItems() return items; } - private static Collection CreateTestCollection() + private static HierarchicalCollection CreateTestHierarchicalCollection() { - var storageRoot = new Collection + var collection = new Collection { Id = "some-id", CustomerId = 1, @@ -197,10 +213,37 @@ private static Collection CreateTestCollection() }, Created = DateTime.MinValue, Modified = DateTime.MinValue, + FullPath = "top/some-id" + }; + + var hierarchy = new Hierarchy() + { + CollectionId = "some-id", + Slug = "root", Parent = "top", + CustomerId = 1, + Type = ResourceType.StorageCollection + }; + + return new HierarchicalCollection(collection, hierarchy); + } + + private static Collection CreateTestCollection() + { + var collection = new Collection + { + Id = "some-id", + CustomerId = 1, + Slug = "root", + Label = new LanguageMap + { + { "en", new List { "repository root" } } + }, + Created = DateTime.MinValue, + Modified = DateTime.MinValue, FullPath = "top/some-id" }; - return storageRoot; + return collection; } } \ No newline at end of file diff --git a/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs b/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs index 974bd491..3a9e0693 100644 --- a/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs +++ b/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs @@ -1,6 +1,7 @@ using API.Converters; using API.Helpers; using Models.Database.Collections; +using Models.Database.General; namespace API.Tests.Helpers; @@ -69,15 +70,14 @@ public void GenerateFlatCollectionId_CreatesId() public void GenerateFlatCollectionParent_CreatesParentId() { // Arrange - var collection = new Collection() + var hierarchy = new Hierarchy() { - Id = "test", Slug = "slug", Parent = "parent" }; // Act - var id = collection.GenerateFlatCollectionParent(urlRoots); + var id = hierarchy.GenerateFlatCollectionParent(urlRoots); // Assert id.Should().Be("http://base/0/collections/parent"); diff --git a/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs index d9424c5e..cd3edad9 100644 --- a/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs @@ -14,6 +14,7 @@ using Models.API.Collection.Upsert; using Models.API.General; using Models.Database.Collections; +using Models.Database.General; using Models.Infrastucture; using Repository; using Test.Helpers.Helpers; @@ -75,15 +76,18 @@ public async Task CreateCollection_CreatesCollection_WhenAllValuesProvided() var responseCollection = await response.ReadAsPresentationResponseAsync(); - var fromDatabase = dbContext.Collections.First(c => c.Id == responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last()); + var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); + + var fromDatabase = dbContext.Collections.First(c => c.Id == id); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.ResourceId == id); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); fromDatabase.Id.Length.Should().BeGreaterThan(6); - fromDatabase.Parent.Should().Be(parent); + hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("test collection"); fromDatabase.Slug.Should().Be("programmatic-child"); - fromDatabase.ItemsOrder.Should().Be(1); + hierarchyFromDatabase.ItemsOrder.Should().Be(1); fromDatabase.Thumbnail.Should().Be("some/thumbnail"); fromDatabase.Tags.Should().Be("some, tags"); fromDatabase.IsPublic.Should().BeTrue(); @@ -130,8 +134,10 @@ public async Task CreateCollection_CreatesCollection_WhenIsStorageCollectionFals var responseCollection = await response.ReadAsPresentationResponseAsync(); - var fromDatabase = dbContext.Collections.First(c => - c.Id == responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last()); + var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); + + var fromDatabase = dbContext.Collections.First(c => c.Id == id); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.ResourceId == id); var fromS3 = await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, @@ -139,10 +145,10 @@ await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); - fromDatabase.Parent.Should().Be(parent); + hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("iiif post"); fromDatabase.Slug.Should().Be("iiif-child"); - fromDatabase.ItemsOrder.Should().Be(1); + hierarchyFromDatabase.ItemsOrder.Should().Be(1); fromDatabase.Tags.Should().Be("some, tags"); fromDatabase.IsPublic.Should().BeTrue(); fromDatabase.IsStorageCollection.Should().BeFalse(); @@ -400,10 +406,18 @@ public async Task UpdateCollection_UpdatesCollection_WhenAllValuesProvided() Tags = "some, tags", IsStorageCollection = true, IsPublic = false, - CustomerId = 1, - Parent = "root" + CustomerId = 1 }; + await dbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "UpdateTester", + Slug = "update-test", + Parent = RootCollection.Id, + Type = ResourceType.StorageCollection, + CustomerId = 1 + }); + await dbContext.Collections.AddAsync(initialCollection); await dbContext.SaveChangesAsync(); @@ -438,14 +452,17 @@ public async Task UpdateCollection_UpdatesCollection_WhenAllValuesProvided() var responseCollection = await response.ReadAsPresentationResponseAsync(); - var fromDatabase = dbContext.Collections.First(c => c.Id == responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last()); + var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); + + var fromDatabase = dbContext.Collections.First(c => c.Id == id); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.ResourceId == id); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); - fromDatabase.Parent.Should().Be(parent); + hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("test collection - updated"); fromDatabase.Slug.Should().Be("programmatic-child"); - fromDatabase.ItemsOrder.Should().Be(1); + hierarchyFromDatabase.ItemsOrder.Should().Be(1); fromDatabase.Thumbnail.Should().Be("some/location/2"); fromDatabase.Tags.Should().Be("some, tags, 2"); fromDatabase.IsPublic.Should().BeTrue(); @@ -482,15 +499,18 @@ public async Task UpdateCollection_CreatesCollection_WhenUnknownCollectionIdProv var responseCollection = await response.ReadAsPresentationResponseAsync(); - var fromDatabase = dbContext.Collections.First(c => c.Id == responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last()); + var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); + + var fromDatabase = dbContext.Collections.First(c => c.Id == id); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.ResourceId == id); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); fromDatabase.Id.Should().Be("createFromUpdate"); - fromDatabase.Parent.Should().Be(parent); + hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("test collection - create from update"); fromDatabase.Slug.Should().Be("create-from-update"); - fromDatabase.ItemsOrder.Should().Be(1); + hierarchyFromDatabase.ItemsOrder.Should().Be(1); fromDatabase.Thumbnail.Should().Be("some/location/2"); fromDatabase.Tags.Should().Be("some, tags, 2"); fromDatabase.IsPublic.Should().BeTrue(); @@ -552,10 +572,18 @@ public async Task UpdateCollection_UpdatesCollection_WhenAllValuesProvidedWithou Tags = "some, tags", IsStorageCollection = true, IsPublic = false, - CustomerId = 1, - Parent = "root" + CustomerId = 1 }; + await dbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "UpdateTester-2", + Slug = "update-test-2", + Parent = RootCollection.Id, + Type = ResourceType.StorageCollection, + CustomerId = 1 + }); + await dbContext.Collections.AddAsync(initialCollection); await dbContext.SaveChangesAsync(); @@ -586,11 +614,14 @@ public async Task UpdateCollection_UpdatesCollection_WhenAllValuesProvidedWithou var responseCollection = await response.ReadAsPresentationResponseAsync(); - var fromDatabase = dbContext.Collections.First(c => c.Id == responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last()); + var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); + + var fromDatabase = dbContext.Collections.First(c => c.Id == id); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.ResourceId == id); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); - fromDatabase.Parent.Should().Be(parent); + hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Slug.Should().Be("programmatic-child-2"); fromDatabase.Label.Should().BeNull(); fromDatabase.IsPublic.Should().BeTrue(); @@ -617,10 +648,18 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenNotStorageCollect Tags = "some, tags", IsStorageCollection = true, IsPublic = false, - CustomerId = 1, - Parent = "root" + CustomerId = 1 }; + await dbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "UpdateTester-3", + Slug = "update-test-3", + Parent = RootCollection.Id, + Type = ResourceType.StorageCollection, + CustomerId = 1 + }); + await dbContext.Collections.AddAsync(initialCollection); await dbContext.SaveChangesAsync(); @@ -673,10 +712,18 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenParentDoesNotExis Tags = "some, tags", IsStorageCollection = true, IsPublic = false, - CustomerId = 1, - Parent = "root" + CustomerId = 1 }; + await dbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "UpdateTester-4", + Slug = "update-test-4", + Parent = RootCollection.Id, + Type = ResourceType.StorageCollection, + CustomerId = 1 + }); + await dbContext.Collections.AddAsync(initialCollection); await dbContext.SaveChangesAsync(); @@ -744,7 +791,7 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenETagIncorrect() [Fact] public async Task UpdateCollection_FailsToUpdateCollection_WhenChangingParentToChild() { - var parentCollection = new Collection() + var parentCollection = new Collection { Id = "UpdateTester-5", Slug = "update-test-5", @@ -760,11 +807,19 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenChangingParentToC Tags = "some, tags", IsStorageCollection = true, IsPublic = false, - CustomerId = 1, - Parent = "root" + CustomerId = 1 }; - var childCollection = new Collection() + await dbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "UpdateTester-5", + Slug = "update-test-5", + Parent = RootCollection.Id, + Type = ResourceType.StorageCollection, + CustomerId = 1 + }); + + var childCollection = new Collection { Id = "UpdateTester-6", Slug = "update-test-6", @@ -780,10 +835,18 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenChangingParentToC Tags = "some, tags", IsStorageCollection = true, IsPublic = false, - CustomerId = 1, - Parent = parentCollection.Id + CustomerId = 1 }; + await dbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "UpdateTester-6", + Slug = "update-test-6", + Parent = parentCollection.Id, + Type = ResourceType.StorageCollection, + CustomerId = 1 + }); + var updatedCollection = new UpsertFlatCollection() { Behavior = new List() @@ -903,10 +966,18 @@ public async Task DeleteCollection_DeletesCollection_WhenAllValuesProvided() Tags = "some, tags", IsStorageCollection = true, IsPublic = false, - CustomerId = 1, - Parent = "root" + CustomerId = 1 }; + await dbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "DeleteTester", + Slug = "delete-test", + Parent = RootCollection.Id, + Type = ResourceType.StorageCollection, + CustomerId = 1 + }); + await dbContext.Collections.AddAsync(initialCollection); await dbContext.SaveChangesAsync(); @@ -1018,8 +1089,10 @@ public async Task CreateCollection_CreatesMinimalCollection_ViaHierarchicalColle var responseCollection = await response.ReadAsIIIFJsonAsync(); - var fromDatabase = dbContext.Collections.First(c => - c.Slug == slug); + var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); + + var fromDatabase = dbContext.Collections.First(c => c.Slug == slug); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.Slug == slug); var fromS3 = await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, @@ -1028,7 +1101,7 @@ await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); responseCollection!.Items.Should().BeNull(); - fromDatabase.Parent.Should().Be(parent); + hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("iiif hierarchical post"); fromDatabase.Slug.Should().Be(slug); fromDatabase.Thumbnail.Should().BeNull(); @@ -1080,8 +1153,10 @@ public async Task CreateCollection_CreatesCollectionWithThumbnailAndItems_ViaHie var responseCollection = await response.ReadAsIIIFJsonAsync(); - var fromDatabase = dbContext.Collections.First(c => - c.Slug == slug); + var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); + + var fromDatabase = dbContext.Collections.First(c => c.Slug == slug); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.Slug == id); var fromS3 = await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, @@ -1089,9 +1164,9 @@ await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); - responseCollection!.Items!.Count.Should().Be(1); + responseCollection.Items!.Count.Should().Be(1); responseCollection.Thumbnail.Should().NotBeNull(); - fromDatabase.Parent.Should().Be(parent); + hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("iiif hierarchical post"); fromDatabase.Slug.Should().Be(slug); fromDatabase.Thumbnail.Should().Be("https://example.org/img/thumb.jpg"); diff --git a/src/IIIFPresentation/API/API.csproj b/src/IIIFPresentation/API/API.csproj index 476ad863..ce82a9c8 100644 --- a/src/IIIFPresentation/API/API.csproj +++ b/src/IIIFPresentation/API/API.csproj @@ -27,9 +27,6 @@ - - .dockerignore - Always diff --git a/src/IIIFPresentation/API/Converters/CollectionConverter.cs b/src/IIIFPresentation/API/Converters/CollectionConverter.cs index 323cf9d2..8d66b45d 100644 --- a/src/IIIFPresentation/API/Converters/CollectionConverter.cs +++ b/src/IIIFPresentation/API/Converters/CollectionConverter.cs @@ -1,4 +1,5 @@ -using API.Helpers; +using API.Features.Storage.Models; +using API.Helpers; using Core.Helpers; using IIIF.Presentation; using IIIF.Presentation.V3; @@ -30,7 +31,7 @@ public static Collection ToHierarchicalCollection(this Models.Database.Collectio return collection; } - public static PresentationCollection ToFlatCollection(this Models.Database.Collections.Collection dbAsset, + public static PresentationCollection ToFlatCollection(this HierarchicalCollection dbAsset, UrlRoots urlRoots, int pageSize, int currentPage, int totalItems, List? items, string? orderQueryParam = null) { @@ -40,23 +41,23 @@ public static PresentationCollection ToFlatCollection(this Models.Database.Colle return new() { - Id = dbAsset.GenerateFlatCollectionId(urlRoots), + Id = dbAsset.Collection.GenerateFlatCollectionId(urlRoots), Context = new List { "http://tbc.org/iiif-repository/1/context.json", "http://iiif.io/api/presentation/3/context.json" }, - Label = dbAsset.Label, - PublicId = dbAsset.GenerateHierarchicalCollectionId(urlRoots), + Label = dbAsset.Collection.Label, + PublicId = dbAsset.Collection.GenerateHierarchicalCollectionId(urlRoots), Behavior = new List() - .AppendIf(dbAsset.IsPublic, Behavior.IsPublic) - .AppendIf(dbAsset.IsStorageCollection, Behavior.IsStorageCollection), - Slug = dbAsset.Slug, - Parent = dbAsset.Parent != null - ? dbAsset.GenerateFlatCollectionParent(urlRoots) + .AppendIf(dbAsset.Collection.IsPublic, Behavior.IsPublic) + .AppendIf(dbAsset.Collection.IsStorageCollection, Behavior.IsStorageCollection), + Slug = dbAsset.Collection.Slug, + Parent = dbAsset.Hierarchy.Parent != null + ? dbAsset.Hierarchy.GenerateFlatCollectionParent(urlRoots) : null, - ItemsOrder = dbAsset.ItemsOrder, + ItemsOrder = dbAsset.Hierarchy.ItemsOrder, Items = items != null ? items.Select(i => (ICollectionItem) new Collection() { @@ -65,42 +66,42 @@ public static PresentationCollection ToFlatCollection(this Models.Database.Colle }).ToList() : [], - PartOf = dbAsset.Parent != null + PartOf = dbAsset.Hierarchy.Parent != null ? [ new PartOf(nameof(PresentationType.Collection)) { - Id = $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}/{dbAsset.Parent}", - Label = dbAsset.Label + Id = $"{urlRoots.BaseUrl}/{dbAsset.Collection.CustomerId}/{dbAsset.Hierarchy.Parent}", + Label = dbAsset.Collection.Label } ] : null, TotalItems = totalItems, - View = GenerateView(dbAsset, urlRoots, pageSize, currentPage, totalPages, orderQueryParamConverted), + View = GenerateView(dbAsset.Collection, urlRoots, pageSize, currentPage, totalPages, orderQueryParamConverted), SeeAlso = [ new(nameof(PresentationType.Collection)) { - Id = dbAsset.GenerateHierarchicalCollectionId(urlRoots), - Label = dbAsset.Label, + Id = dbAsset.Collection.GenerateHierarchicalCollectionId(urlRoots), + Label = dbAsset.Collection.Label, Profile = "Public" }, new(nameof(PresentationType.Collection)) { - Id = $"{dbAsset.GenerateHierarchicalCollectionId(urlRoots)}/iiif", - Label = dbAsset.Label, + Id = $"{dbAsset.Collection.GenerateHierarchicalCollectionId(urlRoots)}/iiif", + Label = dbAsset.Collection.Label, Profile = "api-hierarchical" } ], - Created = dbAsset.Created.Floor(DateTimeX.Precision.Second), - Modified = dbAsset.Modified.Floor(DateTimeX.Precision.Second), - CreatedBy = dbAsset.CreatedBy, - ModifiedBy = dbAsset.ModifiedBy + Created = dbAsset.Collection.Created.Floor(DateTimeX.Precision.Second), + Modified = dbAsset.Collection.Modified.Floor(DateTimeX.Precision.Second), + CreatedBy = dbAsset.Collection.CreatedBy, + ModifiedBy = dbAsset.Collection.ModifiedBy }; } diff --git a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs index ed46f983..eb1bd319 100644 --- a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs +++ b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs @@ -4,6 +4,7 @@ using Models.API.Collection; using Models.API.General; using Models.Database.Collections; +using Models.Database.General; using Repository; using Repository.Helpers; @@ -49,4 +50,24 @@ public static class PresentationContextX return collection; } + + public static async Task RetrieveHierarchyAsync(this PresentationContext dbContext, int customerId, + string resourceId, ResourceType resourceType, CancellationToken cancellationToken = default) + { + var hierarchy = await dbContext.Hierarchy.AsNoTracking().FirstAsync( + s => s.CustomerId == customerId && s.ResourceId == resourceId && s.Type == resourceType, + cancellationToken); + + return hierarchy; + } + + public static IQueryable RetrieveHierarchicalItems(this PresentationContext dbContext, int customerId, string resourceId) + { + + var hierarchicalItems = dbContext.Hierarchy.AsNoTracking() + .Where(h => h.CustomerId == customerId && h.Parent == resourceId).Select(x => x.ResourceId); + return dbContext.Collections + .Where(s => s.CustomerId == customerId && + hierarchicalItems.Contains(s.Id)); + } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Models/CollectionWithItems.cs b/src/IIIFPresentation/API/Features/Storage/Models/CollectionWithItems.cs index f1110cf0..255a6416 100644 --- a/src/IIIFPresentation/API/Features/Storage/Models/CollectionWithItems.cs +++ b/src/IIIFPresentation/API/Features/Storage/Models/CollectionWithItems.cs @@ -1,15 +1,19 @@ -using Models.Database.Collections; +using Models.API.Collection; +using Models.Database.Collections; +using Models.Database.General; namespace API.Features.Storage.Models; public class CollectionWithItems( Collection? collection, + Hierarchy? hierarchy, List? items, int totalItems, string? storedCollection = null) { public Collection? Collection { get; init; } = collection; public List? Items { get; init; } = items; + public Hierarchy? Hierarchy { get; init; } = hierarchy; public int TotalItems { get; init; } = totalItems; public string? StoredCollection { get; init; } = storedCollection; diff --git a/src/IIIFPresentation/API/Features/Storage/Models/HierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Models/HierarchicalCollection.cs new file mode 100644 index 00000000..c999629e --- /dev/null +++ b/src/IIIFPresentation/API/Features/Storage/Models/HierarchicalCollection.cs @@ -0,0 +1,11 @@ +using Models.Database.Collections; +using Models.Database.General; + +namespace API.Features.Storage.Models; + +public class HierarchicalCollection(Collection collection, Hierarchy hierarchy) +{ + public Collection Collection { get; set; } = collection; + + public Hierarchy Hierarchy { get; set; } = hierarchy; +} \ 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 c69e6825..24f320e2 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -2,6 +2,7 @@ using API.Auth; using API.Converters; using API.Features.Storage.Helpers; +using API.Features.Storage.Models; using API.Helpers; using API.Infrastructure.Requests; using API.Settings; @@ -17,6 +18,7 @@ using Models.API.Collection.Upsert; using Models.API.General; using Models.Database.Collections; +using Models.Database.General; using Repository; using Repository.Helpers; using IIdGenerator = API.Infrastructure.IdGenerator.IIdGenerator; @@ -71,17 +73,28 @@ public async Task(request.CustomerId, logger, @@ -116,13 +130,15 @@ await dbContext.TrySaveCollection(request.CustomerId, lo await UploadToS3IfRequiredAsync(request.Collection, collection, convertedIIIFCollection!, cancellationToken); - if (collection.Parent != null) + if (hierarchy.Parent != null) { collection.FullPath = CollectionRetrieval.RetrieveFullPathForCollection(collection, dbContext); } + var hierarchicalCollection = new HierarchicalCollection(collection, hierarchy); + return ModifyEntityResult.Success( - collection.ToFlatCollection(request.UrlRoots, settings.PageSize, CurrentPage, 0, []), // there can be no items attached to this, as it's just been created + hierarchicalCollection.ToFlatCollection(request.UrlRoots, settings.PageSize, CurrentPage, 0, []), // there can be no items attached to this, as it's just been created WriteResult.Created); } diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs index 7977c929..b989dca5 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs @@ -2,6 +2,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Models.API.General; +using Models.Database.General; using Repository; namespace API.Features.Storage.Requests; @@ -31,20 +32,18 @@ public async Task> Handle(Dele DeleteCollectionType.CannotDeleteRootCollection, "Cannot delete a root collection"); } - var collection = await dbContext.Collections.FirstOrDefaultAsync( - c => c.Id == request.CollectionId && c.CustomerId == request.CustomerId, - cancellationToken: cancellationToken); - - if (collection is null) return new ResultMessage(DeleteResult.NotFound); - - if (collection.Parent is null) + var hierarchy = await dbContext.Hierarchy.FirstOrDefaultAsync( + c => c.ResourceId == request.CollectionId && c.CustomerId == request.CustomerId && + c.Type == ResourceType.StorageCollection, cancellationToken); + + if (hierarchy?.Parent is null) { return new ResultMessage(DeleteResult.BadRequest, DeleteCollectionType.CannotDeleteRootCollection, "Cannot delete a root collection"); } - var hasItems = await dbContext.Collections.AnyAsync( - c => c.CustomerId == request.CustomerId && c.Parent == collection.Id, + var hasItems = await dbContext.Hierarchy.AnyAsync( + c => c.CustomerId == request.CustomerId && c.Parent == hierarchy.ResourceId, cancellationToken: cancellationToken); if (hasItems) @@ -53,7 +52,9 @@ public async Task> Handle(Dele DeleteCollectionType.CollectionNotEmpty, "Cannot delete a collection with child items"); } - dbContext.Collections.Remove(collection); + await dbContext.Collections.Where(c => c.Id == hierarchy.ResourceId && c.CustomerId == request.CustomerId) + .ExecuteDeleteAsync(cancellationToken); + dbContext.Hierarchy.Remove(hierarchy); try { await dbContext.SaveChangesAsync(cancellationToken); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs index 75cd624f..4a1f160f 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs @@ -4,6 +4,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Models.Database.Collections; +using Models.Database.General; using Repository; using Repository.Collections; using Repository.Helpers; @@ -21,8 +22,8 @@ public class GetCollection( public int CustomerId { get; } = customerId; public string Id { get; } = id; - public int Page { get; set; } = page; - public int PageSize { get; set; } = pageSize; + public int Page { get; } = page; + public int PageSize { get; } = pageSize; public string? OrderBy { get; } = orderBy; public bool Descending { get; } = descending; } @@ -33,17 +34,20 @@ public async Task Handle(GetCollection request, CancellationToken cancellationToken) { Collection? collection = await dbContext.RetrieveCollection(request.CustomerId, request.Id, cancellationToken); - List? items = null; + Hierarchy? hierarchy = null; int total = 0; if (collection != null) { - total = await dbContext.Collections.CountAsync( + hierarchy = await dbContext.RetrieveHierarchyAsync(collection.CustomerId, collection.Id, + collection.IsStorageCollection ? ResourceType.StorageCollection : ResourceType.IIIFCollection, + cancellationToken); + + total = await dbContext.Hierarchy.CountAsync( c => c.CustomerId == request.CustomerId && c.Parent == collection.Id, cancellationToken: cancellationToken); - items = await dbContext.Collections.AsNoTracking() - .Where(c => c.CustomerId == request.CustomerId && c.Parent == collection.Id) + items = await dbContext.RetrieveHierarchicalItems(request.CustomerId, collection.Id) .AsOrderedCollectionQuery(request.OrderBy, request.Descending) .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) @@ -51,15 +55,15 @@ public async Task Handle(GetCollection request, foreach (var item in items) { - item.FullPath = collection.GenerateFullPath(item.Slug); + item.FullPath = hierarchy.GenerateFullPath(item.Slug); } - if (collection.Parent != null) + if (hierarchy.Parent != null) { collection.FullPath = CollectionRetrieval.RetrieveFullPathForCollection(collection, dbContext); } } - return new CollectionWithItems(collection, items, total); + return new CollectionWithItems(collection, hierarchy, items, total); } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs index 5f086e01..70434026 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs @@ -1,4 +1,6 @@ -using API.Features.Storage.Models; +using System.Diagnostics; +using API.Features.Storage.Helpers; +using API.Features.Storage.Models; using API.Helpers; using AWS.S3; using AWS.S3.Models; @@ -8,6 +10,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Models.Database.Collections; +using Models.Database.General; using Repository; using Repository.Helpers; @@ -29,15 +32,15 @@ public class GetHierarchicalCollectionHandler(PresentationContext dbContext, IBu public async Task Handle(GetHierarchicalCollection request, CancellationToken cancellationToken) { - var collection = - await dbContext.RetriveHierarchicalCollection(request.CustomerId, request.Slug, cancellationToken); - + var hierarchy = + await dbContext.RetrieveHierarchy(request.CustomerId, request.Slug, cancellationToken); + Collection? collection = null; List? items = null; string? collectionFromS3 = null; - if (collection != null) + if (hierarchy?.ResourceId != null) { - if (!collection.IsStorageCollection) + if (hierarchy.Type != ResourceType.StorageCollection) { var objectFromS3 = await bucketReader.GetObjectFromBucket(new ObjectInBucket(settings.S3.StorageBucket, collection.GetCollectionBucketKey()), cancellationToken); @@ -50,16 +53,20 @@ public async Task Handle(GetHierarchicalCollection request, } else { - items = await dbContext.Collections - .Where(s => s.CustomerId == request.CustomerId && s.Parent == collection.Id) - .ToListAsync(cancellationToken: cancellationToken); + collection = await dbContext.RetrieveCollection(request.CustomerId, hierarchy.ResourceId, cancellationToken); - items.ForEach(item => item.FullPath = collection.GenerateFullPath(item.Slug)); - } + if (collection != null) + { + items = await dbContext.RetrieveHierarchicalItems(request.CustomerId, collection.Id) + .ToListAsync(cancellationToken: cancellationToken); - collection.FullPath = request.Slug; + items.ForEach(item => item.FullPath = hierarchy.GenerateFullPath(item.Slug)); + + collection.FullPath = request.Slug; + } + } } - return new CollectionWithItems(collection, items, items?.Count ?? 0, collectionFromS3); + return new CollectionWithItems(collection, hierarchy, items, items?.Count ?? 0, collectionFromS3); } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs index 40649ede..4db8df07 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs @@ -14,6 +14,7 @@ using MediatR; using Microsoft.Extensions.Options; using Models.API.General; +using Models.Database.General; using Repository; using Repository.Helpers; using DatabaseCollection = Models.Database.Collections; @@ -56,7 +57,8 @@ public async Task> Handle(P var parentSlug = string.Join(string.Empty, splitSlug.Take(..^1)); var parentCollection = - await dbContext.RetriveHierarchicalCollection(request.CustomerId, parentSlug, cancellationToken); + await dbContext.RetrieveHierarchy(request.CustomerId, parentSlug, cancellationToken); + if (parentCollection == null) return ErrorHelper.NullParentResponse(); var id = await GenerateUniqueId(request, cancellationToken); @@ -79,7 +81,7 @@ await bucketWriter.WriteToBucket( collection.GetCollectionBucketKey()), collectionFromBody.AsJson(), "application/json", cancellationToken); - if (collection.Parent != null) + if (collection.Hierarchy!.Single(h => h.Canonical).Parent != null) { collection.FullPath = CollectionRetrieval.RetrieveFullPathForCollection(collection, dbContext); } @@ -96,8 +98,6 @@ private static DatabaseCollection.Collection CreateDatabaseCollection(PostHierar var collection = new DatabaseCollection.Collection { Id = id, - Parent = parentCollection.Id, - Slug = splitSlug.Last(), Created = dateCreated, Modified = dateCreated, CreatedBy = Authorizer.GetUser(), @@ -105,8 +105,22 @@ private static DatabaseCollection.Collection CreateDatabaseCollection(PostHierar IsPublic = collectionFromBody.Behavior != null && collectionFromBody.Behavior.IsPublic(), IsStorageCollection = false, Label = collectionFromBody.Label, - Thumbnail = thumbnails!?.GetThumbnailPath() + Thumbnail = thumbnails!?.GetThumbnailPath(), + Hierarchy = [ + new Hierarchy + { + CollectionId = id, + Type = ResourceType.IIIFCollection, + Slug = splitSlug.Last(), + CustomerId = request.CustomerId, + Canonical = true, + ItemsOrder = 0, // items order required? + Parent = parentCollection.Hierarchy.Single(h => h.Canonical).CollectionId, + Public = collectionFromBody.Behavior != null && collectionFromBody.Behavior.IsPublic(), + } + ] }; + return collection; } diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs index fd5e3ca3..7f0bdf3c 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs @@ -1,6 +1,8 @@ using API.Auth; using API.Converters; using API.Features.Storage.Helpers; +using API.Features.Storage.Models; +using API.Helpers; using API.Infrastructure.Helpers; using API.Infrastructure.Requests; using API.Settings; @@ -14,6 +16,7 @@ using Models.API.Collection.Upsert; using Models.API.General; using Models.Database.Collections; +using Models.Database.General; using Repository; using Repository.Helpers; @@ -50,6 +53,8 @@ public async Task c.Id == request.CollectionId, cancellationToken); + + Hierarchy hierarchy; if (databaseCollection == null) { @@ -77,14 +82,25 @@ public async Task(request.CustomerId, lo { return saveErrors; } - - var total = await dbContext.Collections.CountAsync( + + var total = await dbContext.Hierarchy.CountAsync( c => c.CustomerId == request.CustomerId && c.Parent == databaseCollection.Id, cancellationToken: cancellationToken); - - var items = dbContext.Collections - .Where(s => s.CustomerId == request.CustomerId && s.Parent == databaseCollection.Id) + var items = dbContext.RetrieveHierarchicalItems(request.CustomerId, databaseCollection.Id) .Take(settings.PageSize); foreach (var item in items) { - item.FullPath = $"{(databaseCollection.Parent != null ? $"{databaseCollection.Slug}/" : string.Empty)}{item.Slug}"; + item.FullPath = hierarchy.GenerateFullPath(item.Slug); } - if (databaseCollection.Parent != null) + if (hierarchy.Parent != null) { try { @@ -162,9 +183,11 @@ await dbContext.TrySaveCollection(request.CustomerId, lo } await transaction.CommitAsync(cancellationToken); + + var hierarchicalCollection = new HierarchicalCollection(databaseCollection, hierarchy); return ModifyEntityResult.Success( - databaseCollection.ToFlatCollection(request.UrlRoots, settings.PageSize, DefaultCurrentPage, total, + hierarchicalCollection.ToFlatCollection(request.UrlRoots, settings.PageSize, DefaultCurrentPage, total, await items.ToListAsync(cancellationToken: cancellationToken))); } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/StorageController.cs b/src/IIIFPresentation/API/Features/Storage/StorageController.cs index e32698d8..c44d9cfd 100644 --- a/src/IIIFPresentation/API/Features/Storage/StorageController.cs +++ b/src/IIIFPresentation/API/Features/Storage/StorageController.cs @@ -2,6 +2,7 @@ using API.Attributes; using API.Auth; using API.Converters; +using API.Features.Storage.Models; using API.Features.Storage.Requests; using API.Features.Storage.Validators; using API.Helpers; @@ -72,14 +73,14 @@ public async Task Get(int customerId, string id, int? page = 1, i var storageRoot = await Mediator.Send(new GetCollection(customerId, id, page.Value, pageSize.Value, orderByField, descending)); - if (storageRoot.Collection == null) return this.PresentationNotFound(); + if (storageRoot.Collection == null || storageRoot.Hierarchy == null) return this.PresentationNotFound(); if (Request.HasShowExtraHeader() && await authenticator.ValidateRequest(Request) == AuthResult.Success) { var orderByParameter = orderByField != null ? $"{(descending ? nameof(orderByDescending) : nameof(orderBy))}={orderByField}" : null; - + return Ok(storageRoot.Collection.ToFlatCollection(GetUrlRoots(), pageSize.Value, page.Value, storageRoot.TotalItems, storageRoot.Items, orderByParameter)); } diff --git a/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs b/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs index 95ec533f..47c70523 100644 --- a/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs +++ b/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs @@ -3,6 +3,7 @@ using API.Infrastructure.IdGenerator; using Microsoft.EntityFrameworkCore; using Models.Database.Collections; +using Models.Database.General; namespace API.Helpers; @@ -19,8 +20,8 @@ public static string GenerateHierarchicalCollectionId(this Collection collection public static string GenerateFlatCollectionId(this Collection collection, UrlRoots urlRoots) => $"{urlRoots.BaseUrl}/{collection.CustomerId}/collections/{collection.Id}"; - public static string GenerateFlatCollectionParent(this Collection collection, UrlRoots urlRoots) => - $"{urlRoots.BaseUrl}/{collection.CustomerId}/collections/{collection.Parent}"; + public static string GenerateFlatCollectionParent(this Hierarchy hierarchy, UrlRoots urlRoots) => + $"{urlRoots.BaseUrl}/{hierarchy.CustomerId}/collections/{hierarchy.Parent}"; public static string GenerateFlatCollectionViewId(this Collection collection, UrlRoots urlRoots, int currentPage, int pageSize, string? orderQueryParam) => @@ -46,12 +47,12 @@ public static Uri GenerateFlatCollectionViewLast(this Collection collection, Url new( $"{collection.GenerateFlatCollectionId(urlRoots)}?page={lastPage}&pageSize={pageSize}{orderQueryParam}"); + public static string GenerateFullPath(this Hierarchy hierarchy, string itemSlug) => + $"{(hierarchy.Parent != null ? $"{hierarchy.Slug}/" : string.Empty)}{itemSlug}"; + public static string GetCollectionBucketKey(this Collection collection) => $"{collection.CustomerId}/collections/{collection.Id}"; - public static string GenerateFullPath(this Collection collection, string itemSlug) => - $"{(collection.Parent != null ? $"{collection.Slug}/" : string.Empty)}{itemSlug}"; - public static async Task GenerateUniqueIdAsync(this DbSet collections, int customerId, IIdGenerator idGenerator, CancellationToken cancellationToken = default) { diff --git a/src/IIIFPresentation/Models/API/Collection/PresentationCollection.cs b/src/IIIFPresentation/Models/API/Collection/PresentationCollection.cs index 13f1ebc8..ca18a756 100644 --- a/src/IIIFPresentation/Models/API/Collection/PresentationCollection.cs +++ b/src/IIIFPresentation/Models/API/Collection/PresentationCollection.cs @@ -13,10 +13,11 @@ public class PresentationCollection : IIIF.Presentation.V3.Collection public string? PublicId { get; set; } - [JsonRequired] public string? Slug { get; set; } - + [JsonRequired] + public string? Slug { get; set; } + public string? Parent { get; set; } - + public int? ItemsOrder { get; set; } public int TotalItems { get; set; } diff --git a/src/IIIFPresentation/Models/Database/Collections/Collection.cs b/src/IIIFPresentation/Models/Database/Collections/Collection.cs index 7f1562c9..647a7e33 100644 --- a/src/IIIFPresentation/Models/Database/Collections/Collection.cs +++ b/src/IIIFPresentation/Models/Database/Collections/Collection.cs @@ -1,17 +1,18 @@ using System.ComponentModel.DataAnnotations.Schema; using IIIF.Presentation.V3.Strings; +using Models.Database.General; namespace Models.Database.Collections; public class Collection { - public required string Id { get; set; } + public required string Id { get; set; } /// /// Path element /// public required string Slug { get; set; } - + /// /// Whether the id (URL) of the stored Collection is its fixed id, or is the path from parent slugs. Each will redirect to the other if requested on the "wrong" canonical URL. /// @@ -20,53 +21,53 @@ public class Collection /// /// id of parent collection (Storage Collection or IIIF Collection) /// - public string? Parent { get; set; } - + // public string? Parent { get; set; } + /// /// Order within parent collection (unused if parent is storage) /// - public int? ItemsOrder { get; set; } + //public int? ItemsOrder { get; set; } /// /// Derived from the stored IIIF collection JSON - a single value on the default language /// public LanguageMap? Label { get; set; } - + /// /// Not the IIIF JSON, just a single path or URI to 100px, for rapid query results /// public string? Thumbnail { get; set; } - + /// /// User id if being edited /// public string? LockedBy { get; set; } - + /// /// Created date/time /// public DateTime Created { get; set; } - + /// /// Last modified date/time /// public DateTime Modified { get; set; } - + /// /// Who last committed a change to this Collection /// public string? CreatedBy { get; set; } - + /// /// Who last committed a change to this Collection /// public string? ModifiedBy { get; set; } - + /// /// Arbitrary strings to tag manifest, used to create virtual collections /// public string? Tags { get; set; } - + /// /// Is proper IIIF collection; will have JSON in S3 /// @@ -76,12 +77,12 @@ public class Collection /// Whether the collection is available at Presentation.io/iiif/ /// public bool IsPublic { get; set; } - + /// /// The customer identifier /// public int CustomerId { get; set; } - + /// /// The full path to this object, based on parent collections /// diff --git a/src/IIIFPresentation/Models/Database/Collections/Manifest.cs b/src/IIIFPresentation/Models/Database/Collections/Manifest.cs deleted file mode 100644 index 424c0d67..00000000 --- a/src/IIIFPresentation/Models/Database/Collections/Manifest.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Models.Database.Collections; - -public class Manifest -{ - public required string Id { get; set; } -} \ No newline at end of file diff --git a/src/IIIFPresentation/Repository/Collections/CollectionQueryX.cs b/src/IIIFPresentation/Repository/Collections/CollectionQueryX.cs index 85d9dffa..171375b5 100644 --- a/src/IIIFPresentation/Repository/Collections/CollectionQueryX.cs +++ b/src/IIIFPresentation/Repository/Collections/CollectionQueryX.cs @@ -1,5 +1,9 @@ using Core.Helpers; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query.Internal; using Models.Database.Collections; +using Models.Database.General; +using Repository.Helpers; namespace Repository.Collections; diff --git a/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs b/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs index 34921c15..f9235759 100644 --- a/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs +++ b/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs @@ -1,6 +1,7 @@ using Core.Exceptions; using Microsoft.EntityFrameworkCore; using Models.Database.Collections; +using Models.Database.General; namespace Repository.Helpers; @@ -55,7 +56,7 @@ FROM collections child ) SELECT * FROM parentsearch ORDER BY generation_number DESC "; - var parentCollections = dbContext.Collections + var parentCollections = dbContext.Hierarchy .FromSqlRaw(query) .OrderBy(i => i.CustomerId) .ToList(); @@ -64,16 +65,16 @@ FROM collections child { throw new PresentationException("Parent to child relationship exceeds 1000 records"); } - + var fullPath = string.Join('/', parentCollections .Where(parent => !string.IsNullOrEmpty(parent.Parent)) .Select(parent => parent.Slug)); - + return fullPath; } - - public static async Task RetriveHierarchicalCollection(this PresentationContext dbContext, - int customerId, string slug, CancellationToken cancellationToken) + + public static async Task RetrieveHierarchy(this PresentationContext dbContext, + int customerId, string slug, CancellationToken cancellationToken) { var query = $@" WITH RECURSIVE tree_path AS ( @@ -176,20 +177,20 @@ INNER JOIN AND slug = slug_array[max_level] AND tree_path.customer_id = {customerId}"; - Collection? collection; + Hierarchy? hierarchy; if (slug.Equals(string.Empty)) { - collection = await dbContext.Collections.AsNoTracking().FirstOrDefaultAsync( + hierarchy = await dbContext.Hierarchy.AsNoTracking().FirstOrDefaultAsync( s => s.CustomerId == customerId && s.Parent == null, cancellationToken); } else { - collection = await dbContext.Collections.FromSqlRaw(query).OrderBy(i => i.CustomerId) + hierarchy = await dbContext.Hierarchy.FromSqlRaw(query).OrderBy(i => i.CustomerId) .FirstOrDefaultAsync(cancellationToken); } - return collection; + return hierarchy; } } \ No newline at end of file diff --git a/src/IIIFPresentation/Repository/PresentationContext.cs b/src/IIIFPresentation/Repository/PresentationContext.cs index 061f51c4..99c27783 100644 --- a/src/IIIFPresentation/Repository/PresentationContext.cs +++ b/src/IIIFPresentation/Repository/PresentationContext.cs @@ -19,8 +19,6 @@ public PresentationContext(DbContextOptions options) public virtual DbSet Collections { get; set; } - public virtual DbSet Manifests { get; set; } - public virtual DbSet Hierarchy { get; set; } protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) @@ -35,14 +33,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(entity => { entity.HasKey(e => new {e.Id, e.CustomerId}); - entity.HasIndex(e => new { e.CustomerId, e.Slug, e.Parent }).IsUnique(); + //entity.HasIndex(e => new { e.CustomerId, e.Slug, e.Parent }).IsUnique(); entity.Property(e => e.Label).HasColumnType("jsonb"); }); modelBuilder.Entity(entity => { - entity.HasIndex(e => new { e.Slug, e.Parent, e.CustomerId }); + // cannot have duplicate slugs with the same parent + entity.HasIndex(e => new { e.CustomerId, e.Slug, e.Parent }).IsUnique(); + // only 1 canonical path is allowed per resource + entity + .HasIndex(e => new { e.ResourceId, e.CustomerId, e.Canonical, e.Type }) + .IsUnique() + .HasFilter("canonical is true"); }); } } \ No newline at end of file diff --git a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs index 7ed37d0e..4a94e9ff 100644 --- a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs +++ b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs @@ -1,6 +1,7 @@ using IIIF.Presentation.V3.Strings; using Microsoft.EntityFrameworkCore; using Models.Database.Collections; +using Models.Database.General; using Repository; using Test.Helpers.Helpers; using Testcontainers.PostgreSql; @@ -50,7 +51,17 @@ await DbContext.Collections.AddAsync(new Collection() CustomerId = 1 }); - await DbContext.Collections.AddAsync(new Collection() + await DbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = RootCollection.Id, + Slug = "", + Type = ResourceType.StorageCollection, + CustomerId = 1, + Canonical = true, + Public = true + }); + + await DbContext.Collections.AddAsync(new Collection { Id = "FirstChildCollection", Slug = "first-child", @@ -67,7 +78,17 @@ await DbContext.Collections.AddAsync(new Collection() IsStorageCollection = true, IsPublic = true, CustomerId = 1, - Parent = RootCollection.Id + }); + + await DbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "FirstChildCollection", + Slug = "first-child", + Parent = RootCollection.Id, + Type = ResourceType.StorageCollection, + CustomerId = 1, + Canonical = true, + Public = true }); await DbContext.Collections.AddAsync(new Collection() @@ -86,8 +107,18 @@ await DbContext.Collections.AddAsync(new Collection() Tags = "some, tags", IsStorageCollection = true, IsPublic = true, + CustomerId = 1 + }); + + await DbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "SecondChildCollection", + Slug = "second-child", + Parent = "FirstChildCollection", + Type = ResourceType.StorageCollection, CustomerId = 1, - Parent = "FirstChildCollection" + Canonical = true, + Public = true }); await DbContext.Collections.AddAsync(new Collection() @@ -106,8 +137,18 @@ await DbContext.Collections.AddAsync(new Collection() Tags = "some, tags", IsStorageCollection = true, IsPublic = false, + CustomerId = 1 + }); + + await DbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "NonPublic", + Slug = "non-public", + Parent = RootCollection.Id, + Type = ResourceType.StorageCollection, CustomerId = 1, - Parent = RootCollection.Id + Canonical = true, + Public = false }); await DbContext.Collections.AddAsync(new Collection() @@ -126,8 +167,18 @@ await DbContext.Collections.AddAsync(new Collection() Tags = "some, tags", IsStorageCollection = false, IsPublic = true, + CustomerId = 1 + }); + + await DbContext.Hierarchy.AddAsync(new Hierarchy + { + ResourceId = "IiifCollection", + Slug = "iiif-collection", + Parent = RootCollection.Id, + Type = ResourceType.IIIFCollection, CustomerId = 1, - Parent = RootCollection.Id + Canonical = true, + Public = true }); await DbContext.SaveChangesAsync(); From 76d746c15f9439c4ef63f1509b865795e5756c45 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Tue, 15 Oct 2024 12:12:59 +0100 Subject: [PATCH 04/11] getting tests all working --- .../Integration/ModifyCollectionTests.cs | 52 ++--- .../Storage/Helpers/PresentationContextX.cs | 6 +- .../Storage/Requests/CreateCollection.cs | 1 + .../Storage/Requests/DeleteCollection.cs | 2 + .../Requests/GetHierarchicalCollection.cs | 11 +- .../Models/Database/Collections/Collection.cs | 2 + .../Models/Database/Collections/Manifest.cs | 12 ++ .../Models/Database/General/Hierarchy.cs | 8 +- .../Repository/Helpers/CollectionRetrieval.cs | 129 ++++-------- .../Repository/Helpers/DbUpdateExceptionX.cs | 2 +- .../20241014111545_removeColumns.Designer.cs | 152 ++++++++++++++ .../20241014111545_removeColumns.cs | 92 +++++++++ ...819_addForeignKeyRelationships.Designer.cs | 195 ++++++++++++++++++ ...241014141819_addForeignKeyRelationships.cs | 22 ++ .../PresentationContextModelSnapshot.cs | 59 ++++-- .../Repository/PresentationContext.cs | 22 +- 16 files changed, 626 insertions(+), 141 deletions(-) create mode 100644 src/IIIFPresentation/Models/Database/Collections/Manifest.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.Designer.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.Designer.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.cs diff --git a/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs index cd3edad9..b48fd842 100644 --- a/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs @@ -807,18 +807,20 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenChangingParentToC Tags = "some, tags", IsStorageCollection = true, IsPublic = false, - CustomerId = 1 + CustomerId = 1, + Hierarchy = + [ + new() + { + ResourceId = "UpdateTester-5", + Slug = "update-test-5", + Parent = RootCollection.Id, + Type = ResourceType.StorageCollection, + CustomerId = 1 + } + ] }; - await dbContext.Hierarchy.AddAsync(new Hierarchy - { - ResourceId = "UpdateTester-5", - Slug = "update-test-5", - Parent = RootCollection.Id, - Type = ResourceType.StorageCollection, - CustomerId = 1 - }); - var childCollection = new Collection { Id = "UpdateTester-6", @@ -835,17 +837,23 @@ await dbContext.Hierarchy.AddAsync(new Hierarchy Tags = "some, tags", IsStorageCollection = true, IsPublic = false, - CustomerId = 1 + CustomerId = 1, + Hierarchy = + [ + new() + { + ResourceId = "UpdateTester-6", + Slug = "update-test-6", + Parent = parentCollection.Id, + Type = ResourceType.StorageCollection, + CustomerId = 1 + } + ] }; - await dbContext.Hierarchy.AddAsync(new Hierarchy - { - ResourceId = "UpdateTester-6", - Slug = "update-test-6", - Parent = parentCollection.Id, - Type = ResourceType.StorageCollection, - CustomerId = 1 - }); + await dbContext.Collections.AddAsync(parentCollection); + await dbContext.Collections.AddAsync(childCollection); + await dbContext.SaveChangesAsync(); var updatedCollection = new UpsertFlatCollection() { @@ -859,10 +867,6 @@ await dbContext.Hierarchy.AddAsync(new Hierarchy Parent = childCollection.Id }; - await dbContext.Collections.AddAsync(parentCollection); - await dbContext.Collections.AddAsync(childCollection); - await dbContext.SaveChangesAsync(); - var getRequestMessage = HttpRequestMessageBuilder.GetPrivateRequest(HttpMethod.Get, $"{Customer}/collections/{parentCollection.Id}"); @@ -1116,7 +1120,7 @@ await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, public async Task CreateCollection_CreatesCollectionWithThumbnailAndItems_ViaHierarchicalCollection() { // Arrange - var slug = "iiif-collection-post"; + var slug = "iiif-collection-post-2"; var collection = @"{ ""type"": ""Collection"", diff --git a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs index eb1bd319..21d08a23 100644 --- a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs +++ b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs @@ -35,7 +35,7 @@ public static class PresentationContextX } return ModifyEntityResult.Failure( - $"The collection could not be created", ModifyCollectionType.DuplicateSlugValue, WriteResult.Conflict); + $"The collection could not be created", ModifyCollectionType.Unknown); } return null; @@ -44,7 +44,7 @@ public static class PresentationContextX public static async Task RetrieveCollection(this PresentationContext dbContext, int customerId, string collectionId, CancellationToken cancellationToken) { - var collection = await dbContext.Collections.AsNoTracking().FirstOrDefaultAsync( + var collection = await dbContext.Collections.Include(c => c.Hierarchy).AsNoTracking().FirstOrDefaultAsync( s => s.CustomerId == customerId && s.Id == collectionId, cancellationToken); @@ -54,7 +54,7 @@ public static class PresentationContextX public static async Task RetrieveHierarchyAsync(this PresentationContext dbContext, int customerId, string resourceId, ResourceType resourceType, CancellationToken cancellationToken = default) { - var hierarchy = await dbContext.Hierarchy.AsNoTracking().FirstAsync( + var hierarchy = await dbContext.Hierarchy.FirstAsync( s => s.CustomerId == customerId && s.ResourceId == resourceId && s.Type == resourceType, cancellationToken); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs index 24f320e2..cce92b25 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -78,6 +78,7 @@ public async Task> Handle(Dele c => c.ResourceId == request.CollectionId && c.CustomerId == request.CustomerId && c.Type == ResourceType.StorageCollection, cancellationToken); + if (hierarchy is null) return new ResultMessage(DeleteResult.NotFound); + if (hierarchy?.Parent is null) { return new ResultMessage(DeleteResult.BadRequest, diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs index 70434026..dea25573 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs @@ -34,7 +34,6 @@ public async Task Handle(GetHierarchicalCollection request, { var hierarchy = await dbContext.RetrieveHierarchy(request.CustomerId, request.Slug, cancellationToken); - Collection? collection = null; List? items = null; string? collectionFromS3 = null; @@ -53,20 +52,18 @@ public async Task Handle(GetHierarchicalCollection request, } else { - collection = await dbContext.RetrieveCollection(request.CustomerId, hierarchy.ResourceId, cancellationToken); - - if (collection != null) + if (hierarchy.Collection != null) { - items = await dbContext.RetrieveHierarchicalItems(request.CustomerId, collection.Id) + items = await dbContext.RetrieveHierarchicalItems(request.CustomerId, hierarchy.Collection.Id) .ToListAsync(cancellationToken: cancellationToken); items.ForEach(item => item.FullPath = hierarchy.GenerateFullPath(item.Slug)); - collection.FullPath = request.Slug; + hierarchy.Collection.FullPath = request.Slug; } } } - return new CollectionWithItems(collection, hierarchy, items, items?.Count ?? 0, collectionFromS3); + return new CollectionWithItems(hierarchy?.Collection, hierarchy, items, items?.Count ?? 0, collectionFromS3); } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/Database/Collections/Collection.cs b/src/IIIFPresentation/Models/Database/Collections/Collection.cs index 647a7e33..00fef5a4 100644 --- a/src/IIIFPresentation/Models/Database/Collections/Collection.cs +++ b/src/IIIFPresentation/Models/Database/Collections/Collection.cs @@ -82,6 +82,8 @@ public class Collection /// The customer identifier /// public int CustomerId { get; set; } + + public List? Hierarchy { get; set; } /// /// The full path to this object, based on parent collections diff --git a/src/IIIFPresentation/Models/Database/Collections/Manifest.cs b/src/IIIFPresentation/Models/Database/Collections/Manifest.cs new file mode 100644 index 00000000..9b3b87ad --- /dev/null +++ b/src/IIIFPresentation/Models/Database/Collections/Manifest.cs @@ -0,0 +1,12 @@ +using Models.Database.General; + +namespace Models.Database.Collections; + +public class Manifest +{ + public required string Id { get; set; } + + public required int CustomerId { get; set; } + + public List? Hierarchy { get; set; } +} \ No newline at end of file diff --git a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs index 4f97419b..6e40aeaa 100644 --- a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs +++ b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs @@ -1,5 +1,5 @@ - -using Models.Database.Collections; +using Collection = Models.Database.Collections.Collection; +using Manifest = Models.Database.Collections.Manifest; namespace Models.Database.General; @@ -9,6 +9,10 @@ public class Hierarchy public string? ResourceId { get; set; } + public virtual Collection? Collection { get; set; } + + public virtual Manifest? Manifest { get; set; } + public ResourceType Type { get; set; } public required string Slug { get; set; } diff --git a/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs b/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs index f9235759..5a1103e1 100644 --- a/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs +++ b/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs @@ -13,48 +13,35 @@ public static string RetrieveFullPathForCollection(Collection collection, Presen WITH RECURSIVE parentsearch AS ( select id, + resource_id, parent, customer_id, - created, - modified, - created_by, - modified_by, - is_public, - is_storage_collection, items_order, - label, - locked_by, - tags, - thumbnail, - use_path, slug, + canonical, + public, + type, 0 AS generation_number - FROM collections - WHERE id = '{collection.Id}' + FROM hierarchy + WHERE resource_id = '{collection.Id}' UNION SELECT child.id, + child.resource_id, child.parent, child.customer_id, - child.created, - child.modified, - child.created_by, - child.modified_by, - child.is_public, - child.is_storage_collection, child.items_order, - child.label, - child.locked_by, - child.tags, - child.thumbnail, - child.use_path, child.slug, + child.canonical, + child.public, + child.type, generation_number+1 AS generation_number - FROM collections child - JOIN parentsearch ps ON child.id=ps.parent - WHERE generation_number <= 1000 + FROM hierarchy child + JOIN parentsearch ps ON child.resource_id=ps.parent + WHERE generation_number <= 1000 AND child.customer_id = 1 ) -SELECT * FROM parentsearch ORDER BY generation_number DESC +SELECT * FROM parentsearch ps + ORDER BY generation_number DESC "; var parentCollections = dbContext.Hierarchy .FromSqlRaw(query) @@ -80,45 +67,31 @@ FROM collections child WITH RECURSIVE tree_path AS ( SELECT id, + resource_id, parent, slug, customer_id, - created, - modified, - created_by, - modified_by, - is_public, - is_storage_collection, items_order, - label, - thumbnail, - locked_by, - tags, - use_path, - 1 AS level, + canonical, + public, + type, + 1 AS level, slug_array, array_length(slug_array, 1) AS max_level FROM (SELECT id, + resource_id, parent, slug, customer_id, - created, - modified, - created_by, - modified_by, - is_public, - is_storage_collection, items_order, - label, - locked_by, - tags, - thumbnail, - use_path, + canonical, + public, + type, string_to_array('/{slug}', '/') AS slug_array FROM - collections + hierarchy WHERE slug = (string_to_array('/{slug}', '/'))[1] AND parent IS NULL) AS initial_query @@ -126,68 +99,56 @@ WITH RECURSIVE tree_path AS ( UNION ALL SELECT t.id, + t.resource_id, t.parent, t.slug, t.customer_id, - t.created, - t.modified, - t.created_by, - t.modified_by, - t.is_public, - t.is_storage_collection, t.items_order, - t.label, - t.locked_by, - t.tags, - t.thumbnail, - t.use_path, + t.canonical, + t.public, + t.type, tp.level + 1 AS level, tp.slug_array, tp.max_level FROM - collections t + hierarchy t INNER JOIN - tree_path tp ON t.parent = tp.id + tree_path tp ON t.parent = tp.resource_id WHERE tp.level < tp.max_level AND t.slug = tp.slug_array[tp.level + 1] AND t.customer_id = {customerId} ) SELECT - id, - parent, - customer_id, - created, - modified, - created_by, - modified_by, - is_public, - is_storage_collection, - items_order, - label, - locked_by, - tags, - thumbnail, - use_path, - slug + tree_path.id, + tree_path.resource_id, + tree_path.parent, + tree_path.slug, + tree_path.customer_id, + tree_path.items_order, + tree_path.canonical, + tree_path.public, + tree_path.type FROM tree_path +LEFT JOIN collections c ON tree_path.resource_id = c.id AND tree_path.customer_id = c.customer_id WHERE level = max_level - AND slug = slug_array[max_level] + AND tree_path.slug = slug_array[max_level] AND tree_path.customer_id = {customerId}"; Hierarchy? hierarchy; if (slug.Equals(string.Empty)) { - hierarchy = await dbContext.Hierarchy.AsNoTracking().FirstOrDefaultAsync( + hierarchy = await dbContext.Hierarchy.Include(h => h.Collection).AsNoTracking().FirstOrDefaultAsync( s => s.CustomerId == customerId && s.Parent == null, cancellationToken); } else { - hierarchy = await dbContext.Hierarchy.FromSqlRaw(query).OrderBy(i => i.CustomerId) + hierarchy = await dbContext.Hierarchy + .FromSqlRaw(query).Include(h => h.Collection).OrderBy(i => i.CustomerId) .FirstOrDefaultAsync(cancellationToken); } diff --git a/src/IIIFPresentation/Repository/Helpers/DbUpdateExceptionX.cs b/src/IIIFPresentation/Repository/Helpers/DbUpdateExceptionX.cs index 4238439b..41caeb1a 100644 --- a/src/IIIFPresentation/Repository/Helpers/DbUpdateExceptionX.cs +++ b/src/IIIFPresentation/Repository/Helpers/DbUpdateExceptionX.cs @@ -8,6 +8,6 @@ public static bool IsCustomerIdSlugParentViolation(this DbUpdateException except { return exception.InnerException != null && exception.InnerException.Message.Contains( - "duplicate key value violates unique constraint \"ix_collections_customer_id_slug_parent\""); + "duplicate key value violates unique constraint \"ix_hierarchy_customer_id_slug_parent\""); } } \ No newline at end of file diff --git a/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.Designer.cs b/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.Designer.cs new file mode 100644 index 00000000..03b157f8 --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.Designer.cs @@ -0,0 +1,152 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Repository; + +#nullable disable + +namespace Repository.Migrations +{ + [DbContext(typeof(PresentationContext))] + [Migration("20241014111545_removeColumns")] + partial class removeColumns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Models.Database.Collections.Collection", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("IsStorageCollection") + .HasColumnType("boolean") + .HasColumnName("is_storage_collection"); + + b.Property("Label") + .HasColumnType("jsonb") + .HasColumnName("label"); + + b.Property("LockedBy") + .HasColumnType("text") + .HasColumnName("locked_by"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.Property("Thumbnail") + .HasColumnType("text") + .HasColumnName("thumbnail"); + + b.Property("UsePath") + .HasColumnType("boolean") + .HasColumnName("use_path"); + + b.HasKey("Id", "CustomerId") + .HasName("pk_collections"); + + b.ToTable("collections", (string)null); + }); + + modelBuilder.Entity("Models.Database.General.Hierarchy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Canonical") + .HasColumnType("boolean") + .HasColumnName("canonical"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("ItemsOrder") + .HasColumnType("integer") + .HasColumnName("items_order"); + + b.Property("Parent") + .HasColumnType("text") + .HasColumnName("parent"); + + b.Property("Public") + .HasColumnType("boolean") + .HasColumnName("public"); + + b.Property("ResourceId") + .HasColumnType("text") + .HasColumnName("resource_id"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_hierarchy"); + + b.HasIndex("CustomerId", "Slug", "Parent") + .IsUnique() + .HasDatabaseName("ix_hierarchy_customer_id_slug_parent"); + + b.HasIndex("ResourceId", "CustomerId", "Canonical", "Type") + .IsUnique() + .HasDatabaseName("ix_hierarchy_resource_id_customer_id_canonical_type") + .HasFilter("canonical is true"); + + b.ToTable("hierarchy", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.cs b/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.cs new file mode 100644 index 00000000..19610c17 --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.cs @@ -0,0 +1,92 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repository.Migrations +{ + /// + public partial class removeColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "manifests"); + + migrationBuilder.DropIndex( + name: "ix_hierarchy_slug_parent_customer_id", + table: "hierarchy"); + + migrationBuilder.DropIndex( + name: "ix_collections_customer_id_slug_parent", + table: "collections"); + + migrationBuilder.DropColumn( + name: "items_order", + table: "collections"); + + migrationBuilder.DropColumn( + name: "parent", + table: "collections"); + + migrationBuilder.CreateIndex( + name: "ix_hierarchy_customer_id_slug_parent", + table: "hierarchy", + columns: new[] { "customer_id", "slug", "parent" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_hierarchy_resource_id_customer_id_canonical_type", + table: "hierarchy", + columns: new[] { "resource_id", "customer_id", "canonical", "type" }, + unique: true, + filter: "canonical is true"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_hierarchy_customer_id_slug_parent", + table: "hierarchy"); + + migrationBuilder.DropIndex( + name: "ix_hierarchy_resource_id_customer_id_canonical_type", + table: "hierarchy"); + + migrationBuilder.AddColumn( + name: "items_order", + table: "collections", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "parent", + table: "collections", + type: "text", + nullable: true); + + migrationBuilder.CreateTable( + name: "manifests", + columns: table => new + { + id = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_manifests", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_hierarchy_slug_parent_customer_id", + table: "hierarchy", + columns: new[] { "slug", "parent", "customer_id" }); + + migrationBuilder.CreateIndex( + name: "ix_collections_customer_id_slug_parent", + table: "collections", + columns: new[] { "customer_id", "slug", "parent" }, + unique: true); + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.Designer.cs b/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.Designer.cs new file mode 100644 index 00000000..b23911c9 --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.Designer.cs @@ -0,0 +1,195 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Repository; + +#nullable disable + +namespace Repository.Migrations +{ + [DbContext(typeof(PresentationContext))] + [Migration("20241014141819_addForeignKeyRelationships")] + partial class addForeignKeyRelationships + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Models.Database.Collections.Collection", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("IsStorageCollection") + .HasColumnType("boolean") + .HasColumnName("is_storage_collection"); + + b.Property("Label") + .HasColumnType("jsonb") + .HasColumnName("label"); + + b.Property("LockedBy") + .HasColumnType("text") + .HasColumnName("locked_by"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.Property("Thumbnail") + .HasColumnType("text") + .HasColumnName("thumbnail"); + + b.Property("UsePath") + .HasColumnType("boolean") + .HasColumnName("use_path"); + + b.HasKey("Id", "CustomerId") + .HasName("pk_collections"); + + b.ToTable("collections", (string)null); + }); + + modelBuilder.Entity("Models.Database.Collections.Manifest", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.HasKey("Id", "CustomerId") + .HasName("pk_manifest"); + + b.ToTable("manifest", (string)null); + }); + + modelBuilder.Entity("Models.Database.General.Hierarchy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Canonical") + .HasColumnType("boolean") + .HasColumnName("canonical"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("ItemsOrder") + .HasColumnType("integer") + .HasColumnName("items_order"); + + b.Property("Parent") + .HasColumnType("text") + .HasColumnName("parent"); + + b.Property("Public") + .HasColumnType("boolean") + .HasColumnName("public"); + + b.Property("ResourceId") + .HasColumnType("text") + .HasColumnName("resource_id"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_hierarchy"); + + b.HasIndex("CustomerId", "Slug", "Parent") + .IsUnique() + .HasDatabaseName("ix_hierarchy_customer_id_slug_parent"); + + b.HasIndex("ResourceId", "CustomerId", "Canonical", "Type") + .IsUnique() + .HasDatabaseName("ix_hierarchy_resource_id_customer_id_canonical_type") + .HasFilter("canonical is true"); + + b.ToTable("hierarchy", (string)null); + }); + + modelBuilder.Entity("Models.Database.General.Hierarchy", b => + { + b.HasOne("Models.Database.Collections.Collection", "Collection") + .WithMany("Hierarchy") + .HasForeignKey("ResourceId", "CustomerId") + .HasConstraintName("fk_hierarchy_collections_resource_id_customer_id"); + + b.HasOne("Models.Database.Collections.Manifest", "Manifest") + .WithMany("Hierarchy") + .HasForeignKey("ResourceId", "CustomerId") + .HasConstraintName("fk_hierarchy_manifest_resource_id_customer_id"); + + b.Navigation("Collection"); + + b.Navigation("Manifest"); + }); + + modelBuilder.Entity("Models.Database.Collections.Collection", b => + { + b.Navigation("Hierarchy"); + }); + + modelBuilder.Entity("Models.Database.Collections.Manifest", b => + { + b.Navigation("Hierarchy"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.cs b/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.cs new file mode 100644 index 00000000..1b946c4a --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repository.Migrations +{ + /// + public partial class addForeignKeyRelationships : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs index 00938887..ac01bd8f 100644 --- a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs +++ b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs @@ -48,10 +48,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasColumnName("is_storage_collection"); - b.Property("ItemsOrder") - .HasColumnType("integer") - .HasColumnName("items_order"); - b.Property("Label") .HasColumnType("jsonb") .HasColumnName("label"); @@ -68,10 +64,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("modified_by"); - b.Property("Parent") - .HasColumnType("text") - .HasColumnName("parent"); - b.Property("Slug") .IsRequired() .HasColumnType("text") @@ -92,10 +84,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id", "CustomerId") .HasName("pk_collections"); - b.HasIndex("CustomerId", "Slug", "Parent") - .IsUnique() - .HasDatabaseName("ix_collections_customer_id_slug_parent"); - b.ToTable("collections", (string)null); }); @@ -105,10 +93,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("id"); - b.HasKey("Id") - .HasName("pk_manifests"); + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.HasKey("Id", "CustomerId") + .HasName("pk_manifest"); - b.ToTable("manifests", (string)null); + b.ToTable("manifest", (string)null); }); modelBuilder.Entity("Models.Database.General.Hierarchy", b => @@ -156,11 +148,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id") .HasName("pk_hierarchy"); - b.HasIndex("Slug", "Parent", "CustomerId") - .HasDatabaseName("ix_hierarchy_slug_parent_customer_id"); + b.HasIndex("CustomerId", "Slug", "Parent") + .IsUnique() + .HasDatabaseName("ix_hierarchy_customer_id_slug_parent"); + + b.HasIndex("ResourceId", "CustomerId", "Canonical", "Type") + .IsUnique() + .HasDatabaseName("ix_hierarchy_resource_id_customer_id_canonical_type") + .HasFilter("canonical is true"); b.ToTable("hierarchy", (string)null); }); + + modelBuilder.Entity("Models.Database.General.Hierarchy", b => + { + b.HasOne("Models.Database.Collections.Collection", "Collection") + .WithMany("Hierarchy") + .HasForeignKey("ResourceId", "CustomerId") + .HasConstraintName("fk_hierarchy_collections_resource_id_customer_id"); + + b.HasOne("Models.Database.Collections.Manifest", "Manifest") + .WithMany("Hierarchy") + .HasForeignKey("ResourceId", "CustomerId") + .HasConstraintName("fk_hierarchy_manifest_resource_id_customer_id"); + + b.Navigation("Collection"); + + b.Navigation("Manifest"); + }); + + modelBuilder.Entity("Models.Database.Collections.Collection", b => + { + b.Navigation("Hierarchy"); + }); + + modelBuilder.Entity("Models.Database.Collections.Manifest", b => + { + b.Navigation("Hierarchy"); + }); #pragma warning restore 612, 618 } } diff --git a/src/IIIFPresentation/Repository/PresentationContext.cs b/src/IIIFPresentation/Repository/PresentationContext.cs index 99c27783..04a2dbf3 100644 --- a/src/IIIFPresentation/Repository/PresentationContext.cs +++ b/src/IIIFPresentation/Repository/PresentationContext.cs @@ -33,9 +33,26 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(entity => { entity.HasKey(e => new {e.Id, e.CustomerId}); - //entity.HasIndex(e => new { e.CustomerId, e.Slug, e.Parent }).IsUnique(); entity.Property(e => e.Label).HasColumnType("jsonb"); + + // TODO: is there issues on deletions for hierarchy with manifest/collections with the same key? + entity.HasMany(e => e.Hierarchy) + .WithOne(e => e.Collection) + .HasForeignKey(e => new { e.ResourceId, e.CustomerId }) + .HasPrincipalKey(e => new { e.Id, e.CustomerId }) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => new {e.Id, e.CustomerId}); + + entity.HasMany(e => e.Hierarchy) + .WithOne(e => e.Manifest) + .HasForeignKey(e => new { e.ResourceId, e.CustomerId }) + .HasPrincipalKey(e => new { e.Id, e.CustomerId }) + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(entity => @@ -43,8 +60,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // cannot have duplicate slugs with the same parent entity.HasIndex(e => new { e.CustomerId, e.Slug, e.Parent }).IsUnique(); // only 1 canonical path is allowed per resource - entity - .HasIndex(e => new { e.ResourceId, e.CustomerId, e.Canonical, e.Type }) + entity.HasIndex(e => new { e.ResourceId, e.CustomerId, e.Canonical, e.Type }) .IsUnique() .HasFilter("canonical is true"); }); From ef8726875b1661dee2e41ac4cea14d1038d8caeb Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 16 Oct 2024 13:44:01 +0100 Subject: [PATCH 05/11] Update to get the collections to work with the hierarchy table --- .../Converters/CollectionConverterTests.cs | 77 ++++---- .../Helpers/CollectionHelperXTests.cs | 48 ++++- .../Integration/ModifyCollectionTests.cs | 72 ++++---- .../API/Converters/CollectionConverter.cs | 46 ++--- .../Storage/Helpers/PresentationContextX.cs | 12 +- .../Storage/Models/HierarchicalCollection.cs | 11 -- .../Storage/Requests/CreateCollection.cs | 7 +- .../Storage/Requests/DeleteCollection.cs | 18 +- .../Storage/Requests/GetCollection.cs | 3 +- .../Requests/GetHierarchicalCollection.cs | 7 +- .../Storage/Requests/UpsertCollection.cs | 10 +- .../Models/Database/Collections/Collection.cs | 8 +- .../Models/Database/General/Hierarchy.cs | 6 +- .../Collections/CollectionQueryX.cs | 4 +- .../Repository/Helpers/CollectionRetrieval.cs | 39 ++-- ...241009163208_addHierarchyTable.Designer.cs | 170 ------------------ .../20241009163208_addHierarchyTable.cs | 61 ------- .../20241014111545_removeColumns.Designer.cs | 152 ---------------- .../20241014111545_removeColumns.cs | 92 ---------- ...241014141819_addForeignKeyRelationships.cs | 22 --- ...41016123621_addHierarchyTable.Designer.cs} | 49 ++--- .../20241016123621_addHierarchyTable.cs | 132 ++++++++++++++ .../PresentationContextModelSnapshot.cs | 45 +++-- .../Repository/PresentationContext.cs | 15 +- .../Integration/PresentationContextFixture.cs | 15 +- 25 files changed, 411 insertions(+), 710 deletions(-) delete mode 100644 src/IIIFPresentation/API/Features/Storage/Models/HierarchicalCollection.cs delete mode 100644 src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.Designer.cs delete mode 100644 src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.cs delete mode 100644 src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.Designer.cs delete mode 100644 src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.cs delete mode 100644 src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.cs rename src/IIIFPresentation/Repository/Migrations/{20241014141819_addForeignKeyRelationships.Designer.cs => 20241016123621_addHierarchyTable.Designer.cs} (82%) create mode 100644 src/IIIFPresentation/Repository/Migrations/20241016123621_addHierarchyTable.cs diff --git a/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs b/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs index 5ace58cf..c4c84134 100644 --- a/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs +++ b/src/IIIFPresentation/API.Tests/Converters/CollectionConverterTests.cs @@ -27,13 +27,18 @@ public void ToHierarchicalCollection_ConvertsStorageCollection() { Id = "some-id", CustomerId = 1, - Slug = "root", Label = new LanguageMap { { "en", new List { "repository root" } } }, Created = DateTime.MinValue, - Modified = DateTime.MinValue + Modified = DateTime.MinValue, + Hierarchy = [ + new Hierarchy() + { + Slug = "root" + } + ] }; // Act @@ -72,28 +77,28 @@ public void ToFlatCollection_ConvertsStorageCollection() { Id = "some-id", CustomerId = 1, - Slug = "root", Label = new LanguageMap { { "en", new List { "repository root" } } }, Created = DateTime.MinValue, - Modified = DateTime.MinValue - }; - - var hierarchy = new Hierarchy() - { - CollectionId = "some-id", - Slug = "root", - CustomerId = 1, - Type = ResourceType.StorageCollection + Modified = DateTime.MinValue, + Hierarchy = [ + new Hierarchy() + { + CollectionId = "some-id", + Slug = "root", + CustomerId = 1, + Type = ResourceType.StorageCollection, + Canonical = true + } + ] + }; - - var storageRoot = new HierarchicalCollection(collection, hierarchy); // Act var flatCollection = - storageRoot.ToFlatCollection(urlRoots, pageSize, 1, 1, new List(CreateTestItems())); + collection.ToFlatCollection(urlRoots, pageSize, 1, 1, [..CreateTestItems()]); // Assert flatCollection.Id.Should().Be("http://base/1/collections/some-id"); @@ -154,7 +159,7 @@ public void ToFlatCollection_ConvertsStorageCollection_WithCorrectPaging() // Act var flatCollection = storageRoot.ToFlatCollection(urlRoots, 1, 2, 3, - new List(CreateTestItems()), "orderBy=created"); + [..CreateTestItems()], "orderBy=created"); // Assert flatCollection.Id.Should().Be("http://base/1/collections/some-id"); @@ -186,46 +191,55 @@ private static List CreateTestItems() { Id = "some-child", CustomerId = 1, - Slug = "some-child", Label = new LanguageMap { { "en", new List { "repository root" } } }, Created = DateTime.MinValue, Modified = DateTime.MinValue, - FullPath = "top/some-child" + FullPath = "top/some-child", + Hierarchy = [ + new Hierarchy() + { + CollectionId = "some-child", + Slug = "root", + CustomerId = 1, + Type = ResourceType.StorageCollection + } + ] } }; return items; } - private static HierarchicalCollection CreateTestHierarchicalCollection() + private static Collection CreateTestHierarchicalCollection() { var collection = new Collection { Id = "some-id", CustomerId = 1, - Slug = "root", Label = new LanguageMap { { "en", new List { "repository root" } } }, Created = DateTime.MinValue, Modified = DateTime.MinValue, - FullPath = "top/some-id" - }; - - var hierarchy = new Hierarchy() - { - CollectionId = "some-id", - Slug = "root", - Parent = "top", - CustomerId = 1, - Type = ResourceType.StorageCollection + FullPath = "top/some-id", + Hierarchy = [ + new Hierarchy() + { + CollectionId = "some-id", + Slug = "root", + Parent = "top", + CustomerId = 1, + Type = ResourceType.StorageCollection, + Canonical = true + } + ] }; - return new HierarchicalCollection(collection, hierarchy); + return collection; } private static Collection CreateTestCollection() @@ -234,7 +248,6 @@ private static Collection CreateTestCollection() { Id = "some-id", CustomerId = 1, - Slug = "root", Label = new LanguageMap { { "en", new List { "repository root" } } diff --git a/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs b/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs index 3a9e0693..eb03865b 100644 --- a/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs +++ b/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs @@ -21,7 +21,13 @@ public void GenerateHierarchicalCollectionId_CreatesIdWhenNoFullPath() var collection = new Collection() { Id = "test", - Slug = "slug", + Hierarchy = + [ + new Hierarchy() + { + Slug = "slug" + } + ] }; // Act @@ -38,7 +44,13 @@ public void GenerateHierarchicalCollectionId_CreatesIdWhenFullPath() var collection = new Collection() { Id = "test", - Slug = "slug", + Hierarchy = + [ + new Hierarchy() + { + Slug = "slug" + } + ], FullPath = "top/test" }; @@ -56,7 +68,13 @@ public void GenerateFlatCollectionId_CreatesId() var collection = new Collection() { Id = "test", - Slug = "slug" + Hierarchy = + [ + new Hierarchy() + { + Slug = "slug" + } + ] }; // Act @@ -90,7 +108,13 @@ public void GenerateFlatCollectionViewId_CreatesViewId() var collection = new Collection() { Id = "test", - Slug = "slug" + Hierarchy = + [ + new Hierarchy() + { + Slug = "slug" + } + ] }; // Act @@ -107,7 +131,13 @@ public void GenerateFlatCollectionViewNext_CreatesViewNext() var collection = new Collection() { Id = "test", - Slug = "slug" + Hierarchy = + [ + new Hierarchy() + { + Slug = "slug" + } + ] }; // Act @@ -124,7 +154,13 @@ public void GenerateFlatCollectionViewLast_CreatesViewLast() var collection = new Collection() { Id = "test", - Slug = "slug" + Hierarchy = + [ + new Hierarchy() + { + Slug = "slug" + } + ] }; // Act diff --git a/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs index b48fd842..fe95e45e 100644 --- a/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs @@ -17,6 +17,7 @@ using Models.Database.General; using Models.Infrastucture; using Repository; +using Repository.Helpers; using Test.Helpers.Helpers; using Test.Helpers.Integration; using JsonSerializer = System.Text.Json.JsonSerializer; @@ -45,7 +46,7 @@ public ModifyCollectionTests(StorageFixture storageFixture, PresentationAppFacto httpClient = factory.ConfigureBasicIntegrationTestHttpClient(storageFixture.DbFixture, appFactory => appFactory.WithLocalStack(storageFixture.LocalStackFixture)); - parent = dbContext.Collections.First(x => x.CustomerId == Customer && x.Slug == string.Empty).Id; + parent = dbContext.Collections.First(x => x.CustomerId == Customer && x.Hierarchy!.Any(h => h.Slug == string.Empty)).Id; storageFixture.DbFixture.CleanUp(); } @@ -79,14 +80,14 @@ public async Task CreateCollection_CreatesCollection_WhenAllValuesProvided() var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); var fromDatabase = dbContext.Collections.First(c => c.Id == id); - var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.ResourceId == id); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.CollectionId == id); // Assert response.StatusCode.Should().Be(HttpStatusCode.Created); fromDatabase.Id.Length.Should().BeGreaterThan(6); hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("test collection"); - fromDatabase.Slug.Should().Be("programmatic-child"); + hierarchyFromDatabase.Slug.Should().Be("programmatic-child"); hierarchyFromDatabase.ItemsOrder.Should().Be(1); fromDatabase.Thumbnail.Should().Be("some/thumbnail"); fromDatabase.Tags.Should().Be("some, tags"); @@ -137,7 +138,7 @@ public async Task CreateCollection_CreatesCollection_WhenIsStorageCollectionFals var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); var fromDatabase = dbContext.Collections.First(c => c.Id == id); - var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.ResourceId == id); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.CollectionId == id); var fromS3 = await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, @@ -147,7 +148,7 @@ await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, response.StatusCode.Should().Be(HttpStatusCode.Created); hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("iiif post"); - fromDatabase.Slug.Should().Be("iiif-child"); + hierarchyFromDatabase.Slug.Should().Be("iiif-child"); hierarchyFromDatabase.ItemsOrder.Should().Be(1); fromDatabase.Tags.Should().Be("some, tags"); fromDatabase.IsPublic.Should().BeTrue(); @@ -393,7 +394,6 @@ public async Task UpdateCollection_UpdatesCollection_WhenAllValuesProvided() var initialCollection = new Collection() { Id = "UpdateTester", - Slug = "update-test", UsePath = true, Label = new LanguageMap { @@ -411,11 +411,12 @@ public async Task UpdateCollection_UpdatesCollection_WhenAllValuesProvided() await dbContext.Hierarchy.AddAsync(new Hierarchy { - ResourceId = "UpdateTester", + CollectionId = "UpdateTester", Slug = "update-test", Parent = RootCollection.Id, Type = ResourceType.StorageCollection, - CustomerId = 1 + CustomerId = 1, + Canonical = true }); await dbContext.Collections.AddAsync(initialCollection); @@ -440,7 +441,7 @@ await dbContext.Hierarchy.AddAsync(new Hierarchy Parent = parent, ItemsOrder = 1, PresentationThumbnail = "some/location/2", - Tags = "some, tags, 2", + Tags = "some, tags, 2" }; var updateRequestMessage = HttpRequestMessageBuilder.GetPrivateRequest(HttpMethod.Put, @@ -455,13 +456,13 @@ await dbContext.Hierarchy.AddAsync(new Hierarchy var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); var fromDatabase = dbContext.Collections.First(c => c.Id == id); - var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.ResourceId == id); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.CollectionId == id); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("test collection - updated"); - fromDatabase.Slug.Should().Be("programmatic-child"); + hierarchyFromDatabase.Slug.Should().Be("programmatic-child"); hierarchyFromDatabase.ItemsOrder.Should().Be(1); fromDatabase.Thumbnail.Should().Be("some/location/2"); fromDatabase.Tags.Should().Be("some, tags, 2"); @@ -502,14 +503,14 @@ public async Task UpdateCollection_CreatesCollection_WhenUnknownCollectionIdProv var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); var fromDatabase = dbContext.Collections.First(c => c.Id == id); - var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.ResourceId == id); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.CollectionId == id); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); fromDatabase.Id.Should().Be("createFromUpdate"); hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("test collection - create from update"); - fromDatabase.Slug.Should().Be("create-from-update"); + hierarchyFromDatabase.Slug.Should().Be("create-from-update"); hierarchyFromDatabase.ItemsOrder.Should().Be(1); fromDatabase.Thumbnail.Should().Be("some/location/2"); fromDatabase.Tags.Should().Be("some, tags, 2"); @@ -559,7 +560,6 @@ public async Task UpdateCollection_UpdatesCollection_WhenAllValuesProvidedWithou var initialCollection = new Collection() { Id = "UpdateTester-2", - Slug = "update-test-2", UsePath = true, Label = new LanguageMap { @@ -577,11 +577,12 @@ public async Task UpdateCollection_UpdatesCollection_WhenAllValuesProvidedWithou await dbContext.Hierarchy.AddAsync(new Hierarchy { - ResourceId = "UpdateTester-2", + CollectionId = "UpdateTester-2", Slug = "update-test-2", Parent = RootCollection.Id, Type = ResourceType.StorageCollection, - CustomerId = 1 + CustomerId = 1, + Canonical = true }); await dbContext.Collections.AddAsync(initialCollection); @@ -617,12 +618,12 @@ await dbContext.Hierarchy.AddAsync(new Hierarchy var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); var fromDatabase = dbContext.Collections.First(c => c.Id == id); - var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.ResourceId == id); + var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.CollectionId == id); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); hierarchyFromDatabase.Parent.Should().Be(parent); - fromDatabase.Slug.Should().Be("programmatic-child-2"); + hierarchyFromDatabase.Slug.Should().Be("programmatic-child-2"); fromDatabase.Label.Should().BeNull(); fromDatabase.IsPublic.Should().BeTrue(); fromDatabase.IsStorageCollection.Should().BeTrue(); @@ -635,7 +636,6 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenNotStorageCollect var initialCollection = new Collection() { Id = "UpdateTester-3", - Slug = "update-test-3", UsePath = true, Label = new LanguageMap { @@ -653,7 +653,7 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenNotStorageCollect await dbContext.Hierarchy.AddAsync(new Hierarchy { - ResourceId = "UpdateTester-3", + CollectionId = "UpdateTester-3", Slug = "update-test-3", Parent = RootCollection.Id, Type = ResourceType.StorageCollection, @@ -699,7 +699,6 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenParentDoesNotExis var initialCollection = new Collection() { Id = "UpdateTester-4", - Slug = "update-test-4", UsePath = true, Label = new LanguageMap { @@ -717,11 +716,12 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenParentDoesNotExis await dbContext.Hierarchy.AddAsync(new Hierarchy { - ResourceId = "UpdateTester-4", + CollectionId = "UpdateTester-4", Slug = "update-test-4", Parent = RootCollection.Id, Type = ResourceType.StorageCollection, - CustomerId = 1 + CustomerId = 1, + Canonical = true }); await dbContext.Collections.AddAsync(initialCollection); @@ -794,7 +794,6 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenChangingParentToC var parentCollection = new Collection { Id = "UpdateTester-5", - Slug = "update-test-5", UsePath = true, Label = new LanguageMap { @@ -812,7 +811,8 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenChangingParentToC [ new() { - ResourceId = "UpdateTester-5", + Canonical = true, + CollectionId = "UpdateTester-5", Slug = "update-test-5", Parent = RootCollection.Id, Type = ResourceType.StorageCollection, @@ -824,7 +824,6 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenChangingParentToC var childCollection = new Collection { Id = "UpdateTester-6", - Slug = "update-test-6", UsePath = true, Label = new LanguageMap { @@ -842,7 +841,8 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenChangingParentToC [ new() { - ResourceId = "UpdateTester-6", + Canonical = true, + CollectionId = "UpdateTester-6", Slug = "update-test-6", Parent = parentCollection.Id, Type = ResourceType.StorageCollection, @@ -863,7 +863,7 @@ public async Task UpdateCollection_FailsToUpdateCollection_WhenChangingParentToC Behavior.IsStorageCollection }, Label = new LanguageMap("en", ["test collection - updated"]), - Slug = parentCollection.Slug, + Slug = parentCollection.Hierarchy.Single(h => h.Canonical).Slug, Parent = childCollection.Id }; @@ -957,7 +957,6 @@ public async Task DeleteCollection_DeletesCollection_WhenAllValuesProvided() var initialCollection = new Collection() { Id = "DeleteTester", - Slug = "delete-test", UsePath = true, Label = new LanguageMap { @@ -975,11 +974,12 @@ public async Task DeleteCollection_DeletesCollection_WhenAllValuesProvided() await dbContext.Hierarchy.AddAsync(new Hierarchy { - ResourceId = "DeleteTester", + CollectionId = "DeleteTester", Slug = "delete-test", Parent = RootCollection.Id, Type = ResourceType.StorageCollection, - CustomerId = 1 + CustomerId = 1, + Canonical = true }); await dbContext.Collections.AddAsync(initialCollection); @@ -992,10 +992,12 @@ await dbContext.Hierarchy.AddAsync(new Hierarchy var response = await httpClient.AsCustomer(1).SendAsync(deleteRequestMessage); var fromDatabase = dbContext.Collections.FirstOrDefault(c => c.Id == initialCollection.Id); + var fromDatabaseHierarchy = dbContext.Hierarchy.FirstOrDefault(c => c.CollectionId == initialCollection.Id); // Assert response.StatusCode.Should().Be(HttpStatusCode.NoContent); fromDatabase.Should().BeNull(); + fromDatabaseHierarchy.Should().BeNull(); } [Fact] @@ -1095,7 +1097,7 @@ public async Task CreateCollection_CreatesMinimalCollection_ViaHierarchicalColle var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); - var fromDatabase = dbContext.Collections.First(c => c.Slug == slug); + var fromDatabase = dbContext.Collections.First(c => c.Hierarchy!.Single(h => h.Canonical).Slug == slug); var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.Slug == slug); var fromS3 = @@ -1107,7 +1109,7 @@ await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, responseCollection!.Items.Should().BeNull(); hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("iiif hierarchical post"); - fromDatabase.Slug.Should().Be(slug); + hierarchyFromDatabase.Slug.Should().Be(slug); fromDatabase.Thumbnail.Should().BeNull(); fromDatabase.Tags.Should().BeNull(); fromDatabase.IsPublic.Should().BeTrue(); @@ -1159,7 +1161,7 @@ public async Task CreateCollection_CreatesCollectionWithThumbnailAndItems_ViaHie var id = responseCollection!.Id!.Split('/', StringSplitOptions.TrimEntries).Last(); - var fromDatabase = dbContext.Collections.First(c => c.Slug == slug); + var fromDatabase = dbContext.Collections.First(c => c.Hierarchy!.Single(h => h.Canonical).Slug == slug); var hierarchyFromDatabase = dbContext.Hierarchy.First(h => h.CustomerId == 1 && h.Slug == id); var fromS3 = @@ -1172,7 +1174,7 @@ await amazonS3.GetObjectAsync(LocalStackFixture.StorageBucketName, responseCollection.Thumbnail.Should().NotBeNull(); hierarchyFromDatabase.Parent.Should().Be(parent); fromDatabase.Label!.Values.First()[0].Should().Be("iiif hierarchical post"); - fromDatabase.Slug.Should().Be(slug); + hierarchyFromDatabase.Slug.Should().Be(slug); fromDatabase.Thumbnail.Should().Be("https://example.org/img/thumb.jpg"); fromDatabase.Tags.Should().BeNull(); fromDatabase.IsPublic.Should().BeTrue(); diff --git a/src/IIIFPresentation/API/Converters/CollectionConverter.cs b/src/IIIFPresentation/API/Converters/CollectionConverter.cs index 8d66b45d..d2dcfca2 100644 --- a/src/IIIFPresentation/API/Converters/CollectionConverter.cs +++ b/src/IIIFPresentation/API/Converters/CollectionConverter.cs @@ -5,6 +5,7 @@ using IIIF.Presentation.V3; using Models.API.Collection; using Models.Infrastucture; +using Repository.Helpers; namespace API.Converters; @@ -31,33 +32,34 @@ public static Collection ToHierarchicalCollection(this Models.Database.Collectio return collection; } - public static PresentationCollection ToFlatCollection(this HierarchicalCollection dbAsset, + public static PresentationCollection ToFlatCollection(this Models.Database.Collections.Collection dbAsset, UrlRoots urlRoots, int pageSize, int currentPage, int totalItems, List? items, string? orderQueryParam = null) { var totalPages = (int) Math.Ceiling(totalItems == 0 ? 1 : (double) totalItems / pageSize); var orderQueryParamConverted = string.IsNullOrEmpty(orderQueryParam) ? string.Empty : $"&{orderQueryParam}"; + var hierarchy = dbAsset.Hierarchy!.Single(h => h.Canonical); return new() { - Id = dbAsset.Collection.GenerateFlatCollectionId(urlRoots), + Id = dbAsset.GenerateFlatCollectionId(urlRoots), Context = new List { "http://tbc.org/iiif-repository/1/context.json", "http://iiif.io/api/presentation/3/context.json" }, - Label = dbAsset.Collection.Label, - PublicId = dbAsset.Collection.GenerateHierarchicalCollectionId(urlRoots), + Label = dbAsset.Label, + PublicId = dbAsset.GenerateHierarchicalCollectionId(urlRoots), Behavior = new List() - .AppendIf(dbAsset.Collection.IsPublic, Behavior.IsPublic) - .AppendIf(dbAsset.Collection.IsStorageCollection, Behavior.IsStorageCollection), - Slug = dbAsset.Collection.Slug, - Parent = dbAsset.Hierarchy.Parent != null - ? dbAsset.Hierarchy.GenerateFlatCollectionParent(urlRoots) + .AppendIf(dbAsset.IsPublic, Behavior.IsPublic) + .AppendIf(dbAsset.IsStorageCollection, Behavior.IsStorageCollection), + Slug = hierarchy.Slug, + Parent = hierarchy.Parent != null + ? hierarchy.GenerateFlatCollectionParent(urlRoots) : null, - ItemsOrder = dbAsset.Hierarchy.ItemsOrder, + ItemsOrder = hierarchy.ItemsOrder, Items = items != null ? items.Select(i => (ICollectionItem) new Collection() { @@ -66,42 +68,42 @@ public static PresentationCollection ToFlatCollection(this HierarchicalCollectio }).ToList() : [], - PartOf = dbAsset.Hierarchy.Parent != null + PartOf = hierarchy.Parent != null ? [ new PartOf(nameof(PresentationType.Collection)) { - Id = $"{urlRoots.BaseUrl}/{dbAsset.Collection.CustomerId}/{dbAsset.Hierarchy.Parent}", - Label = dbAsset.Collection.Label + Id = $"{urlRoots.BaseUrl}/{dbAsset.CustomerId}/{hierarchy.Parent}", + Label = dbAsset.Label } ] : null, TotalItems = totalItems, - View = GenerateView(dbAsset.Collection, urlRoots, pageSize, currentPage, totalPages, orderQueryParamConverted), + View = GenerateView(dbAsset, urlRoots, pageSize, currentPage, totalPages, orderQueryParamConverted), SeeAlso = [ new(nameof(PresentationType.Collection)) { - Id = dbAsset.Collection.GenerateHierarchicalCollectionId(urlRoots), - Label = dbAsset.Collection.Label, + Id = dbAsset.GenerateHierarchicalCollectionId(urlRoots), + Label = dbAsset.Label, Profile = "Public" }, new(nameof(PresentationType.Collection)) { - Id = $"{dbAsset.Collection.GenerateHierarchicalCollectionId(urlRoots)}/iiif", - Label = dbAsset.Collection.Label, + Id = $"{dbAsset.GenerateHierarchicalCollectionId(urlRoots)}/iiif", + Label = dbAsset.Label, Profile = "api-hierarchical" } ], - Created = dbAsset.Collection.Created.Floor(DateTimeX.Precision.Second), - Modified = dbAsset.Collection.Modified.Floor(DateTimeX.Precision.Second), - CreatedBy = dbAsset.Collection.CreatedBy, - ModifiedBy = dbAsset.Collection.ModifiedBy + Created = dbAsset.Created.Floor(DateTimeX.Precision.Second), + Modified = dbAsset.Modified.Floor(DateTimeX.Precision.Second), + CreatedBy = dbAsset.CreatedBy, + ModifiedBy = dbAsset.ModifiedBy }; } diff --git a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs index 21d08a23..8fc83ff6 100644 --- a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs +++ b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs @@ -1,7 +1,6 @@ using API.Infrastructure.Requests; using Core; using Microsoft.EntityFrameworkCore; -using Models.API.Collection; using Models.API.General; using Models.Database.Collections; using Models.Database.General; @@ -55,7 +54,7 @@ public static async Task RetrieveHierarchyAsync(this PresentationCont string resourceId, ResourceType resourceType, CancellationToken cancellationToken = default) { var hierarchy = await dbContext.Hierarchy.FirstAsync( - s => s.CustomerId == customerId && s.ResourceId == resourceId && s.Type == resourceType, + s => s.CustomerId == customerId && s.CollectionId == resourceId && s.Type == resourceType, cancellationToken); return hierarchy; @@ -63,11 +62,8 @@ public static async Task RetrieveHierarchyAsync(this PresentationCont public static IQueryable RetrieveHierarchicalItems(this PresentationContext dbContext, int customerId, string resourceId) { - - var hierarchicalItems = dbContext.Hierarchy.AsNoTracking() - .Where(h => h.CustomerId == customerId && h.Parent == resourceId).Select(x => x.ResourceId); - return dbContext.Collections - .Where(s => s.CustomerId == customerId && - hierarchicalItems.Contains(s.Id)); + + return dbContext.Collections.Include(c => c.Hierarchy).AsNoTracking().Where(c => + c.CustomerId == customerId && c.Hierarchy!.Single(h => h.Canonical).Parent == resourceId); } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Models/HierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Models/HierarchicalCollection.cs deleted file mode 100644 index c999629e..00000000 --- a/src/IIIFPresentation/API/Features/Storage/Models/HierarchicalCollection.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Models.Database.Collections; -using Models.Database.General; - -namespace API.Features.Storage.Models; - -public class HierarchicalCollection(Collection collection, Hierarchy hierarchy) -{ - public Collection Collection { get; set; } = collection; - - public Hierarchy Hierarchy { get; set; } = hierarchy; -} \ 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 cce92b25..19dedb66 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -73,7 +73,6 @@ public async Task(request.CustomerId, lo collection.FullPath = CollectionRetrieval.RetrieveFullPathForCollection(collection, dbContext); } - var hierarchicalCollection = new HierarchicalCollection(collection, hierarchy); - return ModifyEntityResult.Success( - hierarchicalCollection.ToFlatCollection(request.UrlRoots, settings.PageSize, CurrentPage, 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); } diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs index 16b02600..39166b26 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs @@ -32,20 +32,21 @@ public async Task> Handle(Dele DeleteCollectionType.CannotDeleteRootCollection, "Cannot delete a root collection"); } - var hierarchy = await dbContext.Hierarchy.FirstOrDefaultAsync( - c => c.ResourceId == request.CollectionId && c.CustomerId == request.CustomerId && - c.Type == ResourceType.StorageCollection, cancellationToken); + var collection = await dbContext.Collections.Include(c => c.Hierarchy).FirstOrDefaultAsync(c => + c.Id == request.CollectionId && c.CustomerId == request.CustomerId, cancellationToken); - if (hierarchy is null) return new ResultMessage(DeleteResult.NotFound); + if (collection is null) return new ResultMessage(DeleteResult.NotFound); - if (hierarchy?.Parent is null) + var hierarchy = collection.Hierarchy!.First(c => c.Canonical); + + if (hierarchy.Parent is null) { return new ResultMessage(DeleteResult.BadRequest, DeleteCollectionType.CannotDeleteRootCollection, "Cannot delete a root collection"); } var hasItems = await dbContext.Hierarchy.AnyAsync( - c => c.CustomerId == request.CustomerId && c.Parent == hierarchy.ResourceId, + c => c.CustomerId == request.CustomerId && c.Parent == hierarchy.CollectionId, cancellationToken: cancellationToken); if (hasItems) @@ -54,9 +55,8 @@ public async Task> Handle(Dele DeleteCollectionType.CollectionNotEmpty, "Cannot delete a collection with child items"); } - await dbContext.Collections.Where(c => c.Id == hierarchy.ResourceId && c.CustomerId == request.CustomerId) - .ExecuteDeleteAsync(cancellationToken); - dbContext.Hierarchy.Remove(hierarchy); + dbContext.Collections.Remove(collection); + try { await dbContext.SaveChangesAsync(cancellationToken); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs index 4a1f160f..168c1ae0 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs @@ -49,13 +49,14 @@ public async Task Handle(GetCollection request, cancellationToken: cancellationToken); items = await dbContext.RetrieveHierarchicalItems(request.CustomerId, collection.Id) .AsOrderedCollectionQuery(request.OrderBy, request.Descending) + .Include(c => c.Hierarchy) .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) .ToListAsync(cancellationToken: cancellationToken); foreach (var item in items) { - item.FullPath = hierarchy.GenerateFullPath(item.Slug); + item.FullPath = hierarchy.GenerateFullPath(item.Hierarchy!.Single(h => h.Canonical).Slug); } if (hierarchy.Parent != null) diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs index dea25573..27a63d27 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using API.Features.Storage.Helpers; +using API.Features.Storage.Helpers; using API.Features.Storage.Models; using API.Helpers; using AWS.S3; @@ -37,7 +36,7 @@ public async Task Handle(GetHierarchicalCollection request, List? items = null; string? collectionFromS3 = null; - if (hierarchy?.ResourceId != null) + if (hierarchy?.CollectionId != null) { if (hierarchy.Type != ResourceType.StorageCollection) { @@ -57,7 +56,7 @@ public async Task Handle(GetHierarchicalCollection request, items = await dbContext.RetrieveHierarchicalItems(request.CustomerId, hierarchy.Collection.Id) .ToListAsync(cancellationToken: cancellationToken); - items.ForEach(item => item.FullPath = hierarchy.GenerateFullPath(item.Slug)); + items.ForEach(item => item.FullPath = hierarchy.GenerateFullPath(item.Hierarchy!.Single(h => h.Canonical).Slug)); hierarchy.Collection.FullPath = request.Slug; } diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs index 7f0bdf3c..20a8429a 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs @@ -82,14 +82,13 @@ public async Task(request.CustomerId, lo foreach (var item in items) { - item.FullPath = hierarchy.GenerateFullPath(item.Slug); + item.FullPath = hierarchy.GenerateFullPath(item.Hierarchy!.Single(h => h.Canonical).Slug); } if (hierarchy.Parent != null) @@ -183,11 +181,9 @@ await dbContext.TrySaveCollection(request.CustomerId, lo } await transaction.CommitAsync(cancellationToken); - - var hierarchicalCollection = new HierarchicalCollection(databaseCollection, hierarchy); return ModifyEntityResult.Success( - hierarchicalCollection.ToFlatCollection(request.UrlRoots, settings.PageSize, DefaultCurrentPage, total, + databaseCollection.ToFlatCollection(request.UrlRoots, settings.PageSize, DefaultCurrentPage, total, await items.ToListAsync(cancellationToken: cancellationToken))); } } \ No newline at end of file diff --git a/src/IIIFPresentation/Models/Database/Collections/Collection.cs b/src/IIIFPresentation/Models/Database/Collections/Collection.cs index 00fef5a4..e679df4c 100644 --- a/src/IIIFPresentation/Models/Database/Collections/Collection.cs +++ b/src/IIIFPresentation/Models/Database/Collections/Collection.cs @@ -8,10 +8,10 @@ public class Collection { public required string Id { get; set; } - /// - /// Path element - /// - public required string Slug { get; set; } + // /// + // /// Path element + // /// + // public required string Slug { get; set; } /// /// Whether the id (URL) of the stored Collection is its fixed id, or is the path from parent slugs. Each will redirect to the other if requested on the "wrong" canonical URL. diff --git a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs index 6e40aeaa..8c20cc89 100644 --- a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs +++ b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs @@ -7,10 +7,14 @@ public class Hierarchy { public int Id { get; set; } - public string? ResourceId { get; set; } + // public string? ResourceId { get; set; } + + public string? CollectionId { get; set; } public virtual Collection? Collection { get; set; } + public string? ManifestId { get; set; } + public virtual Manifest? Manifest { get; set; } public ResourceType Type { get; set; } diff --git a/src/IIIFPresentation/Repository/Collections/CollectionQueryX.cs b/src/IIIFPresentation/Repository/Collections/CollectionQueryX.cs index 171375b5..1216a457 100644 --- a/src/IIIFPresentation/Repository/Collections/CollectionQueryX.cs +++ b/src/IIIFPresentation/Repository/Collections/CollectionQueryX.cs @@ -28,8 +28,8 @@ public static IQueryable AsOrderedCollectionQuery( { "id" => descending ? collectionQuery.OrderByDescending(c => c.Id) : collectionQuery.OrderBy(c => c.Id), "slug" => descending - ? collectionQuery.OrderByDescending(c => c.Slug) - : collectionQuery.OrderBy(c => c.Slug), + ? collectionQuery.OrderByDescending(c => c.Hierarchy!.Single(h => h.Canonical).Slug) + : collectionQuery.OrderBy(c => c.Hierarchy!.Single(h => h.Canonical).Slug), "created" => descending ? collectionQuery.OrderByDescending(c => c.Created) : collectionQuery.OrderBy(c => c.Created), diff --git a/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs b/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs index 5a1103e1..97ba445f 100644 --- a/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs +++ b/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs @@ -13,7 +13,8 @@ public static string RetrieveFullPathForCollection(Collection collection, Presen WITH RECURSIVE parentsearch AS ( select id, - resource_id, + collection_id, + manifest_id, parent, customer_id, items_order, @@ -23,11 +24,12 @@ WITH RECURSIVE parentsearch AS ( type, 0 AS generation_number FROM hierarchy - WHERE resource_id = '{collection.Id}' + WHERE collection_id = '{collection.Id}' UNION SELECT child.id, - child.resource_id, + child.collection_id, + child.manifest_id, child.parent, child.customer_id, child.items_order, @@ -37,8 +39,8 @@ FROM hierarchy child.type, generation_number+1 AS generation_number FROM hierarchy child - JOIN parentsearch ps ON child.resource_id=ps.parent - WHERE generation_number <= 1000 AND child.customer_id = 1 + JOIN parentsearch ps ON child.collection_id=ps.parent + WHERE generation_number <= 1000 AND child.customer_id = {collection.CustomerId} ) SELECT * FROM parentsearch ps ORDER BY generation_number DESC @@ -67,7 +69,8 @@ ORDER BY generation_number DESC WITH RECURSIVE tree_path AS ( SELECT id, - resource_id, + collection_id, + manifest_id, parent, slug, customer_id, @@ -81,7 +84,8 @@ WITH RECURSIVE tree_path AS ( FROM (SELECT id, - resource_id, + collection_id, + manifest_id, parent, slug, customer_id, @@ -99,7 +103,8 @@ WITH RECURSIVE tree_path AS ( UNION ALL SELECT t.id, - t.resource_id, + t.collection_id, + t.manifest_id, t.parent, t.slug, t.customer_id, @@ -113,7 +118,7 @@ UNION ALL FROM hierarchy t INNER JOIN - tree_path tp ON t.parent = tp.resource_id + tree_path tp ON t.parent = tp.collection_id WHERE tp.level < tp.max_level AND t.slug = tp.slug_array[tp.level + 1] @@ -121,7 +126,8 @@ INNER JOIN ) SELECT tree_path.id, - tree_path.resource_id, + tree_path.collection_id, + tree_path.manifest_id, tree_path.parent, tree_path.slug, tree_path.customer_id, @@ -131,7 +137,6 @@ INNER JOIN tree_path.type FROM tree_path -LEFT JOIN collections c ON tree_path.resource_id = c.id AND tree_path.customer_id = c.customer_id WHERE level = max_level AND tree_path.slug = slug_array[max_level] @@ -141,14 +146,18 @@ INNER JOIN if (slug.Equals(string.Empty)) { - hierarchy = await dbContext.Hierarchy.Include(h => h.Collection).AsNoTracking().FirstOrDefaultAsync( - s => s.CustomerId == customerId && s.Parent == null, - cancellationToken); + hierarchy = await dbContext.Hierarchy + .Include(h => h.Collection) + .Include(h => h.Manifest) + .AsNoTracking() + .FirstOrDefaultAsync(s => s.CustomerId == customerId && s.Parent == null, cancellationToken); } else { hierarchy = await dbContext.Hierarchy - .FromSqlRaw(query).Include(h => h.Collection).OrderBy(i => i.CustomerId) + .FromSqlRaw(query) + .Include(h => h.Collection) + .Include(h => h.Manifest).OrderBy(i => i.CustomerId) .FirstOrDefaultAsync(cancellationToken); } diff --git a/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.Designer.cs b/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.Designer.cs deleted file mode 100644 index 45f4914c..00000000 --- a/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.Designer.cs +++ /dev/null @@ -1,170 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Repository; - -#nullable disable - -namespace Repository.Migrations -{ - [DbContext(typeof(PresentationContext))] - [Migration("20241009163208_addHierarchyTable")] - partial class addHierarchyTable - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Models.Database.Collections.Collection", b => - { - b.Property("Id") - .HasColumnType("text") - .HasColumnName("id"); - - b.Property("CustomerId") - .HasColumnType("integer") - .HasColumnName("customer_id"); - - b.Property("Created") - .HasColumnType("timestamp with time zone") - .HasColumnName("created"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsPublic") - .HasColumnType("boolean") - .HasColumnName("is_public"); - - b.Property("IsStorageCollection") - .HasColumnType("boolean") - .HasColumnName("is_storage_collection"); - - b.Property("ItemsOrder") - .HasColumnType("integer") - .HasColumnName("items_order"); - - b.Property("Label") - .HasColumnType("jsonb") - .HasColumnName("label"); - - b.Property("LockedBy") - .HasColumnType("text") - .HasColumnName("locked_by"); - - b.Property("Modified") - .HasColumnType("timestamp with time zone") - .HasColumnName("modified"); - - b.Property("ModifiedBy") - .HasColumnType("text") - .HasColumnName("modified_by"); - - b.Property("Parent") - .HasColumnType("text") - .HasColumnName("parent"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("Tags") - .HasColumnType("text") - .HasColumnName("tags"); - - b.Property("Thumbnail") - .HasColumnType("text") - .HasColumnName("thumbnail"); - - b.Property("UsePath") - .HasColumnType("boolean") - .HasColumnName("use_path"); - - b.HasKey("Id", "CustomerId") - .HasName("pk_collections"); - - b.HasIndex("CustomerId", "Slug", "Parent") - .IsUnique() - .HasDatabaseName("ix_collections_customer_id_slug_parent"); - - b.ToTable("collections", (string)null); - }); - - modelBuilder.Entity("Models.Database.Collections.Manifest", b => - { - b.Property("Id") - .HasColumnType("text") - .HasColumnName("id"); - - b.HasKey("Id") - .HasName("pk_manifests"); - - b.ToTable("manifests", (string)null); - }); - - modelBuilder.Entity("Models.Database.General.Hierarchy", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Canonical") - .HasColumnType("boolean") - .HasColumnName("canonical"); - - b.Property("CustomerId") - .HasColumnType("integer") - .HasColumnName("customer_id"); - - b.Property("ItemsOrder") - .HasColumnType("integer") - .HasColumnName("items_order"); - - b.Property("Parent") - .HasColumnType("text") - .HasColumnName("parent"); - - b.Property("Public") - .HasColumnType("boolean") - .HasColumnName("public"); - - b.Property("ResourceId") - .HasColumnType("text") - .HasColumnName("resource_id"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.HasKey("Id") - .HasName("pk_hierarchy"); - - b.HasIndex("Slug", "Parent", "CustomerId") - .HasDatabaseName("ix_hierarchy_slug_parent_customer_id"); - - b.ToTable("hierarchy", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.cs b/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.cs deleted file mode 100644 index 04f2cb60..00000000 --- a/src/IIIFPresentation/Repository/Migrations/20241009163208_addHierarchyTable.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Repository.Migrations -{ - /// - public partial class addHierarchyTable : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "hierarchy", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - resource_id = table.Column(type: "text", nullable: true), - type = table.Column(type: "integer", nullable: false), - slug = table.Column(type: "text", nullable: false), - parent = table.Column(type: "text", nullable: true), - items_order = table.Column(type: "integer", nullable: true), - @public = table.Column(name: "public", type: "boolean", nullable: false), - canonical = table.Column(type: "boolean", nullable: false), - customer_id = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_hierarchy", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "manifests", - columns: table => new - { - id = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_manifests", x => x.id); - }); - - migrationBuilder.CreateIndex( - name: "ix_hierarchy_slug_parent_customer_id", - table: "hierarchy", - columns: new[] { "slug", "parent", "customer_id" }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "hierarchy"); - - migrationBuilder.DropTable( - name: "manifests"); - } - } -} diff --git a/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.Designer.cs b/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.Designer.cs deleted file mode 100644 index 03b157f8..00000000 --- a/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.Designer.cs +++ /dev/null @@ -1,152 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using Repository; - -#nullable disable - -namespace Repository.Migrations -{ - [DbContext(typeof(PresentationContext))] - [Migration("20241014111545_removeColumns")] - partial class removeColumns - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.4") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Models.Database.Collections.Collection", b => - { - b.Property("Id") - .HasColumnType("text") - .HasColumnName("id"); - - b.Property("CustomerId") - .HasColumnType("integer") - .HasColumnName("customer_id"); - - b.Property("Created") - .HasColumnType("timestamp with time zone") - .HasColumnName("created"); - - b.Property("CreatedBy") - .HasColumnType("text") - .HasColumnName("created_by"); - - b.Property("IsPublic") - .HasColumnType("boolean") - .HasColumnName("is_public"); - - b.Property("IsStorageCollection") - .HasColumnType("boolean") - .HasColumnName("is_storage_collection"); - - b.Property("Label") - .HasColumnType("jsonb") - .HasColumnName("label"); - - b.Property("LockedBy") - .HasColumnType("text") - .HasColumnName("locked_by"); - - b.Property("Modified") - .HasColumnType("timestamp with time zone") - .HasColumnName("modified"); - - b.Property("ModifiedBy") - .HasColumnType("text") - .HasColumnName("modified_by"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("Tags") - .HasColumnType("text") - .HasColumnName("tags"); - - b.Property("Thumbnail") - .HasColumnType("text") - .HasColumnName("thumbnail"); - - b.Property("UsePath") - .HasColumnType("boolean") - .HasColumnName("use_path"); - - b.HasKey("Id", "CustomerId") - .HasName("pk_collections"); - - b.ToTable("collections", (string)null); - }); - - modelBuilder.Entity("Models.Database.General.Hierarchy", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Canonical") - .HasColumnType("boolean") - .HasColumnName("canonical"); - - b.Property("CustomerId") - .HasColumnType("integer") - .HasColumnName("customer_id"); - - b.Property("ItemsOrder") - .HasColumnType("integer") - .HasColumnName("items_order"); - - b.Property("Parent") - .HasColumnType("text") - .HasColumnName("parent"); - - b.Property("Public") - .HasColumnType("boolean") - .HasColumnName("public"); - - b.Property("ResourceId") - .HasColumnType("text") - .HasColumnName("resource_id"); - - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.HasKey("Id") - .HasName("pk_hierarchy"); - - b.HasIndex("CustomerId", "Slug", "Parent") - .IsUnique() - .HasDatabaseName("ix_hierarchy_customer_id_slug_parent"); - - b.HasIndex("ResourceId", "CustomerId", "Canonical", "Type") - .IsUnique() - .HasDatabaseName("ix_hierarchy_resource_id_customer_id_canonical_type") - .HasFilter("canonical is true"); - - b.ToTable("hierarchy", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.cs b/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.cs deleted file mode 100644 index 19610c17..00000000 --- a/src/IIIFPresentation/Repository/Migrations/20241014111545_removeColumns.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Repository.Migrations -{ - /// - public partial class removeColumns : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "manifests"); - - migrationBuilder.DropIndex( - name: "ix_hierarchy_slug_parent_customer_id", - table: "hierarchy"); - - migrationBuilder.DropIndex( - name: "ix_collections_customer_id_slug_parent", - table: "collections"); - - migrationBuilder.DropColumn( - name: "items_order", - table: "collections"); - - migrationBuilder.DropColumn( - name: "parent", - table: "collections"); - - migrationBuilder.CreateIndex( - name: "ix_hierarchy_customer_id_slug_parent", - table: "hierarchy", - columns: new[] { "customer_id", "slug", "parent" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_hierarchy_resource_id_customer_id_canonical_type", - table: "hierarchy", - columns: new[] { "resource_id", "customer_id", "canonical", "type" }, - unique: true, - filter: "canonical is true"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "ix_hierarchy_customer_id_slug_parent", - table: "hierarchy"); - - migrationBuilder.DropIndex( - name: "ix_hierarchy_resource_id_customer_id_canonical_type", - table: "hierarchy"); - - migrationBuilder.AddColumn( - name: "items_order", - table: "collections", - type: "integer", - nullable: true); - - migrationBuilder.AddColumn( - name: "parent", - table: "collections", - type: "text", - nullable: true); - - migrationBuilder.CreateTable( - name: "manifests", - columns: table => new - { - id = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_manifests", x => x.id); - }); - - migrationBuilder.CreateIndex( - name: "ix_hierarchy_slug_parent_customer_id", - table: "hierarchy", - columns: new[] { "slug", "parent", "customer_id" }); - - migrationBuilder.CreateIndex( - name: "ix_collections_customer_id_slug_parent", - table: "collections", - columns: new[] { "customer_id", "slug", "parent" }, - unique: true); - } - } -} diff --git a/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.cs b/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.cs deleted file mode 100644 index 1b946c4a..00000000 --- a/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Repository.Migrations -{ - /// - public partial class addForeignKeyRelationships : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.Designer.cs b/src/IIIFPresentation/Repository/Migrations/20241016123621_addHierarchyTable.Designer.cs similarity index 82% rename from src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.Designer.cs rename to src/IIIFPresentation/Repository/Migrations/20241016123621_addHierarchyTable.Designer.cs index b23911c9..b1301796 100644 --- a/src/IIIFPresentation/Repository/Migrations/20241014141819_addForeignKeyRelationships.Designer.cs +++ b/src/IIIFPresentation/Repository/Migrations/20241016123621_addHierarchyTable.Designer.cs @@ -12,8 +12,8 @@ namespace Repository.Migrations { [DbContext(typeof(PresentationContext))] - [Migration("20241014141819_addForeignKeyRelationships")] - partial class addForeignKeyRelationships + [Migration("20241016123621_addHierarchyTable")] + partial class addHierarchyTable { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -67,11 +67,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("modified_by"); - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - b.Property("Tags") .HasColumnType("text") .HasColumnName("tags"); @@ -101,9 +96,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnName("customer_id"); b.HasKey("Id", "CustomerId") - .HasName("pk_manifest"); + .HasName("pk_manifests"); - b.ToTable("manifest", (string)null); + b.ToTable("manifests", (string)null); }); modelBuilder.Entity("Models.Database.General.Hierarchy", b => @@ -119,6 +114,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasColumnName("canonical"); + b.Property("CollectionId") + .HasColumnType("text") + .HasColumnName("collection_id"); + b.Property("CustomerId") .HasColumnType("integer") .HasColumnName("customer_id"); @@ -127,6 +126,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("items_order"); + b.Property("ManifestId") + .HasColumnType("text") + .HasColumnName("manifest_id"); + b.Property("Parent") .HasColumnType("text") .HasColumnName("parent"); @@ -135,10 +138,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasColumnName("public"); - b.Property("ResourceId") - .HasColumnType("text") - .HasColumnName("resource_id"); - b.Property("Slug") .IsRequired() .HasColumnType("text") @@ -151,29 +150,39 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id") .HasName("pk_hierarchy"); + b.HasIndex("CollectionId", "CustomerId", "Canonical") + .IsUnique() + .HasDatabaseName("ix_hierarchy_collection_id_customer_id_canonical") + .HasFilter("canonical is true"); + b.HasIndex("CustomerId", "Slug", "Parent") .IsUnique() .HasDatabaseName("ix_hierarchy_customer_id_slug_parent"); - b.HasIndex("ResourceId", "CustomerId", "Canonical", "Type") + b.HasIndex("ManifestId", "CustomerId", "Canonical") .IsUnique() - .HasDatabaseName("ix_hierarchy_resource_id_customer_id_canonical_type") + .HasDatabaseName("ix_hierarchy_manifest_id_customer_id_canonical") .HasFilter("canonical is true"); - b.ToTable("hierarchy", (string)null); + b.ToTable("hierarchy", null, t => + { + t.HasCheckConstraint("stop_collection_and_manifest_in_same_record", "num_nonnulls(manifest_id, collection_id) = 1"); + }); }); modelBuilder.Entity("Models.Database.General.Hierarchy", b => { b.HasOne("Models.Database.Collections.Collection", "Collection") .WithMany("Hierarchy") - .HasForeignKey("ResourceId", "CustomerId") - .HasConstraintName("fk_hierarchy_collections_resource_id_customer_id"); + .HasForeignKey("CollectionId", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_hierarchy_collections_collection_id_customer_id"); b.HasOne("Models.Database.Collections.Manifest", "Manifest") .WithMany("Hierarchy") - .HasForeignKey("ResourceId", "CustomerId") - .HasConstraintName("fk_hierarchy_manifest_resource_id_customer_id"); + .HasForeignKey("ManifestId", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_hierarchy_manifests_manifest_id_customer_id"); b.Navigation("Collection"); diff --git a/src/IIIFPresentation/Repository/Migrations/20241016123621_addHierarchyTable.cs b/src/IIIFPresentation/Repository/Migrations/20241016123621_addHierarchyTable.cs new file mode 100644 index 00000000..6a670680 --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241016123621_addHierarchyTable.cs @@ -0,0 +1,132 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Repository.Migrations +{ + /// + public partial class addHierarchyTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_collections_customer_id_slug_parent", + table: "collections"); + + migrationBuilder.DropColumn( + name: "items_order", + table: "collections"); + + migrationBuilder.DropColumn( + name: "parent", + table: "collections"); + + migrationBuilder.DropColumn( + name: "slug", + table: "collections"); + + migrationBuilder.CreateTable( + name: "manifests", + columns: table => new + { + id = table.Column(type: "text", nullable: false), + customer_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_manifests", x => new { x.id, x.customer_id }); + }); + + migrationBuilder.CreateTable( + name: "hierarchy", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + collection_id = table.Column(type: "text", nullable: true), + manifest_id = table.Column(type: "text", nullable: true), + type = table.Column(type: "integer", nullable: false), + slug = table.Column(type: "text", nullable: false), + parent = table.Column(type: "text", nullable: true), + items_order = table.Column(type: "integer", nullable: true), + @public = table.Column(name: "public", type: "boolean", nullable: false), + canonical = table.Column(type: "boolean", nullable: false), + customer_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_hierarchy", x => x.id); + table.CheckConstraint("stop_collection_and_manifest_in_same_record", "num_nonnulls(manifest_id, collection_id) = 1"); + table.ForeignKey( + name: "fk_hierarchy_collections_collection_id_customer_id", + columns: x => new { x.collection_id, x.customer_id }, + principalTable: "collections", + principalColumns: new[] { "id", "customer_id" }, + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_hierarchy_manifests_manifest_id_customer_id", + columns: x => new { x.manifest_id, x.customer_id }, + principalTable: "manifests", + principalColumns: new[] { "id", "customer_id" }, + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_hierarchy_collection_id_customer_id_canonical", + table: "hierarchy", + columns: new[] { "collection_id", "customer_id", "canonical" }, + unique: true, + filter: "canonical is true"); + + migrationBuilder.CreateIndex( + name: "ix_hierarchy_customer_id_slug_parent", + table: "hierarchy", + columns: new[] { "customer_id", "slug", "parent" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_hierarchy_manifest_id_customer_id_canonical", + table: "hierarchy", + columns: new[] { "manifest_id", "customer_id", "canonical" }, + unique: true, + filter: "canonical is true"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "hierarchy"); + + migrationBuilder.DropTable( + name: "manifests"); + + migrationBuilder.AddColumn( + name: "items_order", + table: "collections", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "parent", + table: "collections", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "slug", + table: "collections", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "ix_collections_customer_id_slug_parent", + table: "collections", + columns: new[] { "customer_id", "slug", "parent" }, + unique: true); + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs index ac01bd8f..7c064d5f 100644 --- a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs +++ b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs @@ -64,11 +64,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("modified_by"); - b.Property("Slug") - .IsRequired() - .HasColumnType("text") - .HasColumnName("slug"); - b.Property("Tags") .HasColumnType("text") .HasColumnName("tags"); @@ -98,9 +93,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("customer_id"); b.HasKey("Id", "CustomerId") - .HasName("pk_manifest"); + .HasName("pk_manifests"); - b.ToTable("manifest", (string)null); + b.ToTable("manifests", (string)null); }); modelBuilder.Entity("Models.Database.General.Hierarchy", b => @@ -116,6 +111,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasColumnName("canonical"); + b.Property("CollectionId") + .HasColumnType("text") + .HasColumnName("collection_id"); + b.Property("CustomerId") .HasColumnType("integer") .HasColumnName("customer_id"); @@ -124,6 +123,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("items_order"); + b.Property("ManifestId") + .HasColumnType("text") + .HasColumnName("manifest_id"); + b.Property("Parent") .HasColumnType("text") .HasColumnName("parent"); @@ -132,10 +135,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean") .HasColumnName("public"); - b.Property("ResourceId") - .HasColumnType("text") - .HasColumnName("resource_id"); - b.Property("Slug") .IsRequired() .HasColumnType("text") @@ -148,29 +147,39 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id") .HasName("pk_hierarchy"); + b.HasIndex("CollectionId", "CustomerId", "Canonical") + .IsUnique() + .HasDatabaseName("ix_hierarchy_collection_id_customer_id_canonical") + .HasFilter("canonical is true"); + b.HasIndex("CustomerId", "Slug", "Parent") .IsUnique() .HasDatabaseName("ix_hierarchy_customer_id_slug_parent"); - b.HasIndex("ResourceId", "CustomerId", "Canonical", "Type") + b.HasIndex("ManifestId", "CustomerId", "Canonical") .IsUnique() - .HasDatabaseName("ix_hierarchy_resource_id_customer_id_canonical_type") + .HasDatabaseName("ix_hierarchy_manifest_id_customer_id_canonical") .HasFilter("canonical is true"); - b.ToTable("hierarchy", (string)null); + b.ToTable("hierarchy", null, t => + { + t.HasCheckConstraint("stop_collection_and_manifest_in_same_record", "num_nonnulls(manifest_id, collection_id) = 1"); + }); }); modelBuilder.Entity("Models.Database.General.Hierarchy", b => { b.HasOne("Models.Database.Collections.Collection", "Collection") .WithMany("Hierarchy") - .HasForeignKey("ResourceId", "CustomerId") - .HasConstraintName("fk_hierarchy_collections_resource_id_customer_id"); + .HasForeignKey("CollectionId", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_hierarchy_collections_collection_id_customer_id"); b.HasOne("Models.Database.Collections.Manifest", "Manifest") .WithMany("Hierarchy") - .HasForeignKey("ResourceId", "CustomerId") - .HasConstraintName("fk_hierarchy_manifest_resource_id_customer_id"); + .HasForeignKey("ManifestId", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_hierarchy_manifests_manifest_id_customer_id"); b.Navigation("Collection"); diff --git a/src/IIIFPresentation/Repository/PresentationContext.cs b/src/IIIFPresentation/Repository/PresentationContext.cs index 04a2dbf3..167f8726 100644 --- a/src/IIIFPresentation/Repository/PresentationContext.cs +++ b/src/IIIFPresentation/Repository/PresentationContext.cs @@ -21,6 +21,8 @@ public PresentationContext(DbContextOptions options) public virtual DbSet Hierarchy { get; set; } + public virtual DbSet Manifests { get; set; } + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { configurationBuilder @@ -39,7 +41,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // TODO: is there issues on deletions for hierarchy with manifest/collections with the same key? entity.HasMany(e => e.Hierarchy) .WithOne(e => e.Collection) - .HasForeignKey(e => new { e.ResourceId, e.CustomerId }) + .HasForeignKey(e => new { e.CollectionId, e.CustomerId }) .HasPrincipalKey(e => new { e.Id, e.CustomerId }) .OnDelete(DeleteBehavior.Cascade); }); @@ -50,7 +52,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasMany(e => e.Hierarchy) .WithOne(e => e.Manifest) - .HasForeignKey(e => new { e.ResourceId, e.CustomerId }) + .HasForeignKey(e => new { e.ManifestId, e.CustomerId }) .HasPrincipalKey(e => new { e.Id, e.CustomerId }) .OnDelete(DeleteBehavior.Cascade); }); @@ -60,7 +62,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // cannot have duplicate slugs with the same parent entity.HasIndex(e => new { e.CustomerId, e.Slug, e.Parent }).IsUnique(); // only 1 canonical path is allowed per resource - entity.HasIndex(e => new { e.ResourceId, e.CustomerId, e.Canonical, e.Type }) + entity.HasIndex(e => new { e.ManifestId, e.CustomerId, e.Canonical }) + .IsUnique() + .HasFilter("canonical is true"); + + entity.ToTable(h => h.HasCheckConstraint("stop_collection_and_manifest_in_same_record", + "num_nonnulls(manifest_id, collection_id) = 1")); + + entity.HasIndex(e => new { e.CollectionId, e.CustomerId, e.Canonical }) .IsUnique() .HasFilter("canonical is true"); }); diff --git a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs index 4a94e9ff..a9f323ad 100644 --- a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs +++ b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs @@ -35,7 +35,6 @@ private async Task SeedCustomer() await DbContext.Collections.AddAsync(new Collection() { Id = RootCollection.Id, - Slug = "", UsePath = true, Label = new LanguageMap { @@ -53,7 +52,7 @@ await DbContext.Collections.AddAsync(new Collection() await DbContext.Hierarchy.AddAsync(new Hierarchy { - ResourceId = RootCollection.Id, + CollectionId = RootCollection.Id, Slug = "", Type = ResourceType.StorageCollection, CustomerId = 1, @@ -64,7 +63,6 @@ await DbContext.Hierarchy.AddAsync(new Hierarchy await DbContext.Collections.AddAsync(new Collection { Id = "FirstChildCollection", - Slug = "first-child", UsePath = true, Label = new LanguageMap { @@ -82,7 +80,7 @@ await DbContext.Collections.AddAsync(new Collection await DbContext.Hierarchy.AddAsync(new Hierarchy { - ResourceId = "FirstChildCollection", + CollectionId = "FirstChildCollection", Slug = "first-child", Parent = RootCollection.Id, Type = ResourceType.StorageCollection, @@ -94,7 +92,6 @@ await DbContext.Hierarchy.AddAsync(new Hierarchy await DbContext.Collections.AddAsync(new Collection() { Id = "SecondChildCollection", - Slug = "second-child", UsePath = true, Label = new LanguageMap { @@ -112,7 +109,7 @@ await DbContext.Collections.AddAsync(new Collection() await DbContext.Hierarchy.AddAsync(new Hierarchy { - ResourceId = "SecondChildCollection", + CollectionId = "SecondChildCollection", Slug = "second-child", Parent = "FirstChildCollection", Type = ResourceType.StorageCollection, @@ -124,7 +121,6 @@ await DbContext.Hierarchy.AddAsync(new Hierarchy await DbContext.Collections.AddAsync(new Collection() { Id = "NonPublic", - Slug = "non-public", UsePath = true, Label = new LanguageMap { @@ -142,7 +138,7 @@ await DbContext.Collections.AddAsync(new Collection() await DbContext.Hierarchy.AddAsync(new Hierarchy { - ResourceId = "NonPublic", + CollectionId = "NonPublic", Slug = "non-public", Parent = RootCollection.Id, Type = ResourceType.StorageCollection, @@ -154,7 +150,6 @@ await DbContext.Hierarchy.AddAsync(new Hierarchy await DbContext.Collections.AddAsync(new Collection() { Id = "IiifCollection", - Slug = "iiif-collection", UsePath = true, Label = new LanguageMap { @@ -172,7 +167,7 @@ await DbContext.Collections.AddAsync(new Collection() await DbContext.Hierarchy.AddAsync(new Hierarchy { - ResourceId = "IiifCollection", + CollectionId = "IiifCollection", Slug = "iiif-collection", Parent = RootCollection.Id, Type = ResourceType.IIIFCollection, From e22c60435b3ecdd22e9352cfa8258d051c12b160 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 16 Oct 2024 14:21:18 +0100 Subject: [PATCH 06/11] remove commented code + fix method signature --- .../Requests/PostHierarchicalCollection.cs | 6 +++--- .../Models/Database/Collections/Collection.cs | 15 --------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs index 4db8df07..1962915f 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs @@ -90,7 +90,7 @@ await bucketWriter.WriteToBucket( } private static DatabaseCollection.Collection CreateDatabaseCollection(PostHierarchicalCollection request, Collection collectionFromBody, string id, - DatabaseCollection.Collection parentCollection, string[] splitSlug) + Hierarchy parentHierarchy, string[] splitSlug) { var thumbnails = collectionFromBody.Thumbnail?.Select(x => x as Image).ToList(); @@ -114,8 +114,8 @@ private static DatabaseCollection.Collection CreateDatabaseCollection(PostHierar Slug = splitSlug.Last(), CustomerId = request.CustomerId, Canonical = true, - ItemsOrder = 0, // items order required? - Parent = parentCollection.Hierarchy.Single(h => h.Canonical).CollectionId, + ItemsOrder = 0, + Parent = parentHierarchy.CollectionId, Public = collectionFromBody.Behavior != null && collectionFromBody.Behavior.IsPublic(), } ] diff --git a/src/IIIFPresentation/Models/Database/Collections/Collection.cs b/src/IIIFPresentation/Models/Database/Collections/Collection.cs index e679df4c..2339ed2e 100644 --- a/src/IIIFPresentation/Models/Database/Collections/Collection.cs +++ b/src/IIIFPresentation/Models/Database/Collections/Collection.cs @@ -8,26 +8,11 @@ public class Collection { public required string Id { get; set; } - // /// - // /// Path element - // /// - // public required string Slug { get; set; } - /// /// Whether the id (URL) of the stored Collection is its fixed id, or is the path from parent slugs. Each will redirect to the other if requested on the "wrong" canonical URL. /// public bool UsePath { get; set; } - /// - /// id of parent collection (Storage Collection or IIIF Collection) - /// - // public string? Parent { get; set; } - - /// - /// Order within parent collection (unused if parent is storage) - /// - //public int? ItemsOrder { get; set; } - /// /// Derived from the stored IIIF collection JSON - a single value on the default language /// From 509088e138c94046840319500b7f65dcdfc0f98f Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Wed, 16 Oct 2024 14:59:53 +0100 Subject: [PATCH 07/11] changes before opening for code review --- .../Integration/ModifyCollectionTests.cs | 6 ++--- .../Storage/Helpers/PresentationContextX.cs | 2 +- .../Storage/Requests/CreateCollection.cs | 1 - .../Storage/Requests/DeleteCollection.cs | 1 - .../Storage/Requests/GetCollection.cs | 2 +- .../Requests/GetHierarchicalCollection.cs | 2 +- .../Storage/Requests/UpsertCollection.cs | 3 +-- .../API/Features/Storage/StorageController.cs | 3 +-- .../Collection/Upsert/UpsertFlatCollection.cs | 4 +--- .../Models/Database/General/Hierarchy.cs | 22 ++++++++++++++++--- 10 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs b/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs index fe95e45e..4dbea22d 100644 --- a/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs +++ b/src/IIIFPresentation/API.Tests/Integration/ModifyCollectionTests.cs @@ -1,4 +1,6 @@ -using System.Data; +#nullable disable + +using System.Data; using System.Net; using System.Net.Http.Headers; using System.Text; @@ -9,7 +11,6 @@ using Core.Response; using FakeItEasy; using IIIF.Presentation.V3.Strings; -using Microsoft.AspNetCore.Mvc.Testing; using Models.API.Collection; using Models.API.Collection.Upsert; using Models.API.General; @@ -17,7 +18,6 @@ using Models.Database.General; using Models.Infrastucture; using Repository; -using Repository.Helpers; using Test.Helpers.Helpers; using Test.Helpers.Integration; using JsonSerializer = System.Text.Json.JsonSerializer; diff --git a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs index 8fc83ff6..734dafbd 100644 --- a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs +++ b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs @@ -60,7 +60,7 @@ public static async Task RetrieveHierarchyAsync(this PresentationCont return hierarchy; } - public static IQueryable RetrieveHierarchicalItems(this PresentationContext dbContext, int customerId, string resourceId) + public static IQueryable RetrieveCollectionItems(this PresentationContext dbContext, int customerId, string resourceId) { return dbContext.Collections.Include(c => c.Hierarchy).AsNoTracking().Where(c => diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs index 19dedb66..ad3a1ea9 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -2,7 +2,6 @@ using API.Auth; using API.Converters; using API.Features.Storage.Helpers; -using API.Features.Storage.Models; using API.Helpers; using API.Infrastructure.Requests; using API.Settings; diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs index 39166b26..cdfbf2f6 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs @@ -2,7 +2,6 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Models.API.General; -using Models.Database.General; using Repository; namespace API.Features.Storage.Requests; diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs index 168c1ae0..47e99228 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs @@ -47,7 +47,7 @@ public async Task Handle(GetCollection request, total = await dbContext.Hierarchy.CountAsync( c => c.CustomerId == request.CustomerId && c.Parent == collection.Id, cancellationToken: cancellationToken); - items = await dbContext.RetrieveHierarchicalItems(request.CustomerId, collection.Id) + items = await dbContext.RetrieveCollectionItems(request.CustomerId, collection.Id) .AsOrderedCollectionQuery(request.OrderBy, request.Descending) .Include(c => c.Hierarchy) .Skip((request.Page - 1) * request.PageSize) diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs index 27a63d27..9b06d83e 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs @@ -53,7 +53,7 @@ public async Task Handle(GetHierarchicalCollection request, { if (hierarchy.Collection != null) { - items = await dbContext.RetrieveHierarchicalItems(request.CustomerId, hierarchy.Collection.Id) + items = await dbContext.RetrieveCollectionItems(request.CustomerId, hierarchy.Collection.Id) .ToListAsync(cancellationToken: cancellationToken); items.ForEach(item => item.FullPath = hierarchy.GenerateFullPath(item.Hierarchy!.Single(h => h.Canonical).Slug)); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs index 20a8429a..8aee4870 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs @@ -1,7 +1,6 @@ using API.Auth; using API.Converters; using API.Features.Storage.Helpers; -using API.Features.Storage.Models; using API.Helpers; using API.Infrastructure.Helpers; using API.Infrastructure.Requests; @@ -157,7 +156,7 @@ await dbContext.TrySaveCollection(request.CustomerId, lo var total = await dbContext.Hierarchy.CountAsync( c => c.CustomerId == request.CustomerId && c.Parent == databaseCollection.Id, cancellationToken: cancellationToken); - var items = dbContext.RetrieveHierarchicalItems(request.CustomerId, databaseCollection.Id) + var items = dbContext.RetrieveCollectionItems(request.CustomerId, databaseCollection.Id) .Take(settings.PageSize); foreach (var item in items) diff --git a/src/IIIFPresentation/API/Features/Storage/StorageController.cs b/src/IIIFPresentation/API/Features/Storage/StorageController.cs index c44d9cfd..e5fe9244 100644 --- a/src/IIIFPresentation/API/Features/Storage/StorageController.cs +++ b/src/IIIFPresentation/API/Features/Storage/StorageController.cs @@ -2,7 +2,6 @@ using API.Attributes; using API.Auth; using API.Converters; -using API.Features.Storage.Models; using API.Features.Storage.Requests; using API.Features.Storage.Validators; using API.Helpers; @@ -117,7 +116,7 @@ public async Task Post(int customerId, [FromServices] UpsertFlatC return this.ValidationFailed(validation); } - return await HandleUpsert(new CreateCollection(customerId, collection, rawRequestBody, GetUrlRoots())); + return await HandleUpsert(new CreateCollection(customerId, collection!, rawRequestBody, GetUrlRoots())); } [Authorize] diff --git a/src/IIIFPresentation/Models/API/Collection/Upsert/UpsertFlatCollection.cs b/src/IIIFPresentation/Models/API/Collection/Upsert/UpsertFlatCollection.cs index a48950dc..9f591929 100644 --- a/src/IIIFPresentation/Models/API/Collection/Upsert/UpsertFlatCollection.cs +++ b/src/IIIFPresentation/Models/API/Collection/Upsert/UpsertFlatCollection.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; -using IIIF.Presentation.V3.Content; -using IIIF.Presentation.V3.Strings; +using IIIF.Presentation.V3.Strings; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs index 8c20cc89..71a84f5b 100644 --- a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs +++ b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs @@ -6,9 +6,7 @@ namespace Models.Database.General; public class Hierarchy { public int Id { get; set; } - - // public string? ResourceId { get; set; } - + public string? CollectionId { get; set; } public virtual Collection? Collection { get; set; } @@ -17,16 +15,34 @@ public class Hierarchy public virtual Manifest? Manifest { get; set; } + /// + /// The type of the resource i.e.: storage collection, IIIF collection, manifest, etc + /// public ResourceType Type { get; set; } + /// + /// The slug used on public requests + /// public required string Slug { get; set; } + /// + /// The id of the parent record + /// public string? Parent { get; set; } + /// + /// Used to determine the order of the item when viewed in a collection + /// public int? ItemsOrder { get; set; } + /// + /// Whether the item is publicly available or not + /// public bool Public { get; set; } + /// + /// Whether this record is the canonical path for the collection or hierarchy + /// public bool Canonical { get; set; } /// From 2b3166e2c6a5bab025885f1f20feac91c9633744 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Thu, 17 Oct 2024 14:49:14 +0100 Subject: [PATCH 08/11] fixing code review comments --- .../Helpers/CollectionHelperXTests.cs | 62 ++---- .../Storage/Helpers/PresentationContextX.cs | 63 ++++-- .../Storage/Models/CollectionWithItems.cs | 2 + .../Storage/Requests/CreateCollection.cs | 28 +-- .../Storage/Requests/GetCollection.cs | 47 ++-- .../Requests/PostHierarchicalCollection.cs | 3 +- .../Storage/Requests/UpsertCollection.cs | 28 ++- .../API/Features/Storage/StorageController.cs | 2 +- .../Models/Database/General/Hierarchy.cs | 7 +- .../Repository/Helpers/CollectionRetrieval.cs | 6 - ...3422_removePublicFromHierarchy.Designer.cs | 200 ++++++++++++++++++ ...0241016163422_removePublicFromHierarchy.cs | 29 +++ .../PresentationContextModelSnapshot.cs | 4 - .../Repository/PresentationContext.cs | 1 - .../Integration/PresentationContextFixture.cs | 15 +- 15 files changed, 347 insertions(+), 150 deletions(-) create mode 100644 src/IIIFPresentation/Repository/Migrations/20241016163422_removePublicFromHierarchy.Designer.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20241016163422_removePublicFromHierarchy.cs diff --git a/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs b/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs index eb03865b..dfe6473b 100644 --- a/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs +++ b/src/IIIFPresentation/API.Tests/Helpers/CollectionHelperXTests.cs @@ -18,16 +18,10 @@ public class CollectionHelperXTests public void GenerateHierarchicalCollectionId_CreatesIdWhenNoFullPath() { // Arrange - var collection = new Collection() + var collection = new Collection { Id = "test", - Hierarchy = - [ - new Hierarchy() - { - Slug = "slug" - } - ] + Hierarchy = GetDefaultHierarchyList() }; // Act @@ -41,16 +35,10 @@ public void GenerateHierarchicalCollectionId_CreatesIdWhenNoFullPath() public void GenerateHierarchicalCollectionId_CreatesIdWhenFullPath() { // Arrange - var collection = new Collection() + var collection = new Collection { Id = "test", - Hierarchy = - [ - new Hierarchy() - { - Slug = "slug" - } - ], + Hierarchy = GetDefaultHierarchyList(), FullPath = "top/test" }; @@ -65,16 +53,10 @@ public void GenerateHierarchicalCollectionId_CreatesIdWhenFullPath() public void GenerateFlatCollectionId_CreatesId() { // Arrange - var collection = new Collection() + var collection = new Collection { Id = "test", - Hierarchy = - [ - new Hierarchy() - { - Slug = "slug" - } - ] + Hierarchy = GetDefaultHierarchyList() }; // Act @@ -88,7 +70,7 @@ public void GenerateFlatCollectionId_CreatesId() public void GenerateFlatCollectionParent_CreatesParentId() { // Arrange - var hierarchy = new Hierarchy() + var hierarchy = new Hierarchy { Slug = "slug", Parent = "parent" @@ -105,16 +87,10 @@ public void GenerateFlatCollectionParent_CreatesParentId() public void GenerateFlatCollectionViewId_CreatesViewId() { // Arrange - var collection = new Collection() + var collection = new Collection { Id = "test", - Hierarchy = - [ - new Hierarchy() - { - Slug = "slug" - } - ] + Hierarchy = GetDefaultHierarchyList() }; // Act @@ -128,16 +104,10 @@ public void GenerateFlatCollectionViewId_CreatesViewId() public void GenerateFlatCollectionViewNext_CreatesViewNext() { // Arrange - var collection = new Collection() + var collection = new Collection { Id = "test", - Hierarchy = - [ - new Hierarchy() - { - Slug = "slug" - } - ] + Hierarchy = GetDefaultHierarchyList() }; // Act @@ -154,13 +124,7 @@ public void GenerateFlatCollectionViewLast_CreatesViewLast() var collection = new Collection() { Id = "test", - Hierarchy = - [ - new Hierarchy() - { - Slug = "slug" - } - ] + Hierarchy = GetDefaultHierarchyList() }; // Act @@ -169,4 +133,6 @@ public void GenerateFlatCollectionViewLast_CreatesViewLast() // Assert id.Should().Be("http://base/0/collections/test?page=1&pageSize=10&test"); } + + private static List GetDefaultHierarchyList() => [ new() { Slug = "slug" } ]; } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs index 734dafbd..bad93894 100644 --- a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs +++ b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs @@ -39,31 +39,62 @@ public static class PresentationContextX return null; } - - public static async Task RetrieveCollection(this PresentationContext dbContext, int customerId, - string collectionId, CancellationToken cancellationToken) + + + /// + /// Retrieves a collection from the database, with the Hierarchy records included + /// + /// The context to pull records from + /// Customer the record is attached to + /// The collection to retrieve + /// Whether the resource should be tracked or not + /// A cancellation token + /// The retrieved collection + public static async Task RetrieveCollectionAsync(this PresentationContext dbContext, int customerId, + string collectionId, bool tracked = false, CancellationToken cancellationToken = default) { - var collection = await dbContext.Collections.Include(c => c.Hierarchy).AsNoTracking().FirstOrDefaultAsync( - s => s.CustomerId == customerId && s.Id == collectionId, - cancellationToken); + var collection = tracked ? await dbContext.Collections.Include(c => c.Hierarchy) + .FirstOrDefaultAsync(s => s.CustomerId == customerId && s.Id == collectionId, cancellationToken) + : await dbContext.Collections.Include(c => c.Hierarchy).AsNoTracking() + .FirstOrDefaultAsync(s => s.CustomerId == customerId && s.Id == collectionId, cancellationToken); return collection; } - public static async Task RetrieveHierarchyAsync(this PresentationContext dbContext, int customerId, - string resourceId, ResourceType resourceType, CancellationToken cancellationToken = default) + /// + /// Retrieves child collections from the database of the parent record, while including the hierarchy records + /// + /// The context to pull records from + /// Customer the record is attached to + /// The collection to retrieve child items for + /// Whether the resource should be tracked or not + /// A query containing child collections + public static IQueryable RetrieveCollectionItems(this PresentationContext dbContext, int customerId, + string collectionId, bool tracking = false) { - var hierarchy = await dbContext.Hierarchy.FirstAsync( - s => s.CustomerId == customerId && s.CollectionId == resourceId && s.Type == resourceType, - cancellationToken); - - return hierarchy; + return tracking ? dbContext.Collections.Include(c => c.Hierarchy).Where(c => + c.CustomerId == customerId && c.Hierarchy!.Single(h => h.Canonical).Parent == collectionId) + : dbContext.Collections.Include(c => c.Hierarchy).AsNoTracking().Where(c => + c.CustomerId == customerId && c.Hierarchy!.Single(h => h.Canonical).Parent == collectionId); } - public static IQueryable RetrieveCollectionItems(this PresentationContext dbContext, int customerId, string resourceId) + public static async Task GetTotalItemCountForCollection(this PresentationContext dbContext, Collection collection, + int itemCount, int pageSize, CancellationToken cancellationToken = default) { + int total; + if (itemCount < pageSize) + { + // there can't be more as we've asked for PageSize and got less + total = itemCount; + } + else + { + // if we get PageSize back then there may be more in db + total = await dbContext.Hierarchy.CountAsync( + c => c.CustomerId == collection.CustomerId && c.Parent == collection.Id, + cancellationToken: cancellationToken); + } - return dbContext.Collections.Include(c => c.Hierarchy).AsNoTracking().Where(c => - c.CustomerId == customerId && c.Hierarchy!.Single(h => h.Canonical).Parent == resourceId); + return total; } } \ No newline at end of file diff --git a/src/IIIFPresentation/API/Features/Storage/Models/CollectionWithItems.cs b/src/IIIFPresentation/API/Features/Storage/Models/CollectionWithItems.cs index 255a6416..9b7510ff 100644 --- a/src/IIIFPresentation/API/Features/Storage/Models/CollectionWithItems.cs +++ b/src/IIIFPresentation/API/Features/Storage/Models/CollectionWithItems.cs @@ -25,4 +25,6 @@ public void Deconstruct(out Collection? collection, out List? items, totalItems = TotalItems; storedCollection = StoredCollection; } + + public static CollectionWithItems Empty { get; private set; } = new(null, null, null, 0); } \ 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 ad3a1ea9..c9f20d42 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -29,7 +29,7 @@ public class CreateCollection(int customerId, UpsertFlatCollection collection, s { public int CustomerId { get; } = customerId; - public UpsertFlatCollection Collection { get; } = collection; + public UpsertFlatCollection? Collection { get; } = collection; public string RawRequestBody { get; } = rawRequestBody; @@ -51,10 +51,12 @@ public class CreateCollectionHandler( public async Task> Handle(CreateCollection request, CancellationToken cancellationToken) { // check parent exists - var parentCollection = await dbContext.RetrieveCollection(request.CustomerId, - request.Collection.Parent.GetLastPathElement(), cancellationToken); + var parentCollection = await dbContext.RetrieveCollectionAsync(request.CustomerId, + request.Collection.Parent.GetLastPathElement(), cancellationToken: cancellationToken); if (parentCollection == null) return ErrorHelper.NullParentResponse(); + + var isStorageCollection = request.Collection.Behavior.IsStorageCollection(); string id; @@ -78,27 +80,26 @@ public async Task(request.CustomerId, lo { return saveErrors; } - - await UploadToS3IfRequiredAsync(request.Collection, collection, convertedIIIFCollection!, cancellationToken); + + await UploadToS3IfRequiredAsync(request, collection, convertedIIIFCollection!, isStorageCollection, + cancellationToken); if (hierarchy.Parent != null) { @@ -151,10 +153,10 @@ private static string ConvertToIIIFCollection(CreateCollection request, Collecti return convertedIIIFCollection; } - private async Task UploadToS3IfRequiredAsync(UpsertFlatCollection request, - Collection collection, string convertedIIIFCollection, CancellationToken cancellationToken) + private async Task UploadToS3IfRequiredAsync(CreateCollection request, + Collection collection, string convertedIIIFCollection, bool isStorageCollection, CancellationToken cancellationToken = default) { - if (!request.Behavior.IsStorageCollection()) + if (!isStorageCollection) { await bucketWriter.WriteToBucket( new ObjectInBucket(settings.AWS.S3.StorageBucket, diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs index 47e99228..fef2bef1 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs @@ -33,36 +33,29 @@ public class GetCollectionHandler(PresentationContext dbContext) : IRequestHandl public async Task Handle(GetCollection request, CancellationToken cancellationToken) { - Collection? collection = await dbContext.RetrieveCollection(request.CustomerId, request.Id, cancellationToken); - List? items = null; - Hierarchy? hierarchy = null; - int total = 0; + var collection = await dbContext.RetrieveCollectionAsync(request.CustomerId, request.Id, cancellationToken: cancellationToken); + + if (collection is null) return CollectionWithItems.Empty; - if (collection != null) - { - hierarchy = await dbContext.RetrieveHierarchyAsync(collection.CustomerId, collection.Id, - collection.IsStorageCollection ? ResourceType.StorageCollection : ResourceType.IIIFCollection, - cancellationToken); - - total = await dbContext.Hierarchy.CountAsync( - c => c.CustomerId == request.CustomerId && c.Parent == collection.Id, - cancellationToken: cancellationToken); - items = await dbContext.RetrieveCollectionItems(request.CustomerId, collection.Id) - .AsOrderedCollectionQuery(request.OrderBy, request.Descending) - .Include(c => c.Hierarchy) - .Skip((request.Page - 1) * request.PageSize) - .Take(request.PageSize) - .ToListAsync(cancellationToken: cancellationToken); + var hierarchy = collection.Hierarchy!.Single(h => h.Canonical); + + var items = await dbContext.RetrieveCollectionItems(request.CustomerId, collection.Id) + .AsOrderedCollectionQuery(request.OrderBy, request.Descending) + .Skip((request.Page - 1) * request.PageSize) + .Take(request.PageSize) + .ToListAsync(cancellationToken: cancellationToken); + + var total = await dbContext.GetTotalItemCountForCollection(collection, items.Count, request.PageSize, + cancellationToken); - foreach (var item in items) - { - item.FullPath = hierarchy.GenerateFullPath(item.Hierarchy!.Single(h => h.Canonical).Slug); - } + foreach (var item in items) + { + item.FullPath = hierarchy.GenerateFullPath(item.Hierarchy!.Single(h => h.Canonical).Slug); + } - if (hierarchy.Parent != null) - { - collection.FullPath = CollectionRetrieval.RetrieveFullPathForCollection(collection, dbContext); - } + if (hierarchy.Parent != null) + { + collection.FullPath = CollectionRetrieval.RetrieveFullPathForCollection(collection, dbContext); } return new CollectionWithItems(collection, hierarchy, items, total); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs index 1962915f..f995fbdd 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs @@ -115,8 +115,7 @@ private static DatabaseCollection.Collection CreateDatabaseCollection(PostHierar CustomerId = request.CustomerId, Canonical = true, ItemsOrder = 0, - Parent = parentHierarchy.CollectionId, - Public = collectionFromBody.Behavior != null && collectionFromBody.Behavior.IsPublic(), + Parent = parentHierarchy.CollectionId } ] }; diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs index 8aee4870..09237956 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs @@ -51,7 +51,7 @@ public async Task c.Id == request.CollectionId, cancellationToken); + await dbContext.RetrieveCollectionAsync(request.CustomerId, request.CollectionId, true, cancellationToken); Hierarchy hierarchy; @@ -66,8 +66,8 @@ public async Task(); @@ -93,8 +93,7 @@ public async Task c.Canonical); if (hierarchy.Parent != request.Collection.Parent) { - var parentCollection = await dbContext.RetrieveCollection(request.CustomerId, - request.Collection.Parent.GetLastPathElement(), cancellationToken); + var parentCollection = await dbContext.RetrieveCollectionAsync(request.CustomerId, + request.Collection.Parent.GetLastPathElement(), cancellationToken: cancellationToken); if (parentCollection == null) { @@ -137,7 +134,6 @@ public async Task(request.CustomerId, lo { return saveErrors; } - - var total = await dbContext.Hierarchy.CountAsync( - c => c.CustomerId == request.CustomerId && c.Parent == databaseCollection.Id, - cancellationToken: cancellationToken); - var items = dbContext.RetrieveCollectionItems(request.CustomerId, databaseCollection.Id) + + var items = dbContext + .RetrieveCollectionItems(request.CustomerId, databaseCollection.Id) .Take(settings.PageSize); + + var total = await dbContext.GetTotalItemCountForCollection(databaseCollection, items.Count(), settings.PageSize, cancellationToken); foreach (var item in items) { diff --git a/src/IIIFPresentation/API/Features/Storage/StorageController.cs b/src/IIIFPresentation/API/Features/Storage/StorageController.cs index e5fe9244..50fa3b05 100644 --- a/src/IIIFPresentation/API/Features/Storage/StorageController.cs +++ b/src/IIIFPresentation/API/Features/Storage/StorageController.cs @@ -72,7 +72,7 @@ public async Task Get(int customerId, string id, int? page = 1, i var storageRoot = await Mediator.Send(new GetCollection(customerId, id, page.Value, pageSize.Value, orderByField, descending)); - if (storageRoot.Collection == null || storageRoot.Hierarchy == null) return this.PresentationNotFound(); + if (storageRoot.Collection == null) return this.PresentationNotFound(); if (Request.HasShowExtraHeader() && await authenticator.ValidateRequest(Request) == AuthResult.Success) { diff --git a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs index 71a84f5b..0a8908b7 100644 --- a/src/IIIFPresentation/Models/Database/General/Hierarchy.cs +++ b/src/IIIFPresentation/Models/Database/General/Hierarchy.cs @@ -35,15 +35,10 @@ public class Hierarchy /// public int? ItemsOrder { get; set; } - /// - /// Whether the item is publicly available or not - /// - public bool Public { get; set; } - /// /// Whether this record is the canonical path for the collection or hierarchy /// - public bool Canonical { get; set; } + public bool Canonical { get; set; } = true; /// /// The customer identifier diff --git a/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs b/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs index 97ba445f..91d09951 100644 --- a/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs +++ b/src/IIIFPresentation/Repository/Helpers/CollectionRetrieval.cs @@ -20,7 +20,6 @@ WITH RECURSIVE parentsearch AS ( items_order, slug, canonical, - public, type, 0 AS generation_number FROM hierarchy @@ -35,7 +34,6 @@ FROM hierarchy child.items_order, child.slug, child.canonical, - child.public, child.type, generation_number+1 AS generation_number FROM hierarchy child @@ -76,7 +74,6 @@ WITH RECURSIVE tree_path AS ( customer_id, items_order, canonical, - public, type, 1 AS level, slug_array, @@ -91,7 +88,6 @@ WITH RECURSIVE tree_path AS ( customer_id, items_order, canonical, - public, type, string_to_array('/{slug}', '/') AS slug_array FROM @@ -110,7 +106,6 @@ UNION ALL t.customer_id, t.items_order, t.canonical, - t.public, t.type, tp.level + 1 AS level, tp.slug_array, @@ -133,7 +128,6 @@ INNER JOIN tree_path.customer_id, tree_path.items_order, tree_path.canonical, - tree_path.public, tree_path.type FROM tree_path diff --git a/src/IIIFPresentation/Repository/Migrations/20241016163422_removePublicFromHierarchy.Designer.cs b/src/IIIFPresentation/Repository/Migrations/20241016163422_removePublicFromHierarchy.Designer.cs new file mode 100644 index 00000000..6d6bc1f7 --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241016163422_removePublicFromHierarchy.Designer.cs @@ -0,0 +1,200 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Repository; + +#nullable disable + +namespace Repository.Migrations +{ + [DbContext(typeof(PresentationContext))] + [Migration("20241016163422_removePublicFromHierarchy")] + partial class removePublicFromHierarchy + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Models.Database.Collections.Collection", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("IsPublic") + .HasColumnType("boolean") + .HasColumnName("is_public"); + + b.Property("IsStorageCollection") + .HasColumnType("boolean") + .HasColumnName("is_storage_collection"); + + b.Property("Label") + .HasColumnType("jsonb") + .HasColumnName("label"); + + b.Property("LockedBy") + .HasColumnType("text") + .HasColumnName("locked_by"); + + b.Property("Modified") + .HasColumnType("timestamp with time zone") + .HasColumnName("modified"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("Tags") + .HasColumnType("text") + .HasColumnName("tags"); + + b.Property("Thumbnail") + .HasColumnType("text") + .HasColumnName("thumbnail"); + + b.Property("UsePath") + .HasColumnType("boolean") + .HasColumnName("use_path"); + + b.HasKey("Id", "CustomerId") + .HasName("pk_collections"); + + b.ToTable("collections", (string)null); + }); + + modelBuilder.Entity("Models.Database.Collections.Manifest", b => + { + b.Property("Id") + .HasColumnType("text") + .HasColumnName("id"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.HasKey("Id", "CustomerId") + .HasName("pk_manifests"); + + b.ToTable("manifests", (string)null); + }); + + modelBuilder.Entity("Models.Database.General.Hierarchy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Canonical") + .HasColumnType("boolean") + .HasColumnName("canonical"); + + b.Property("CollectionId") + .HasColumnType("text") + .HasColumnName("collection_id"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("ItemsOrder") + .HasColumnType("integer") + .HasColumnName("items_order"); + + b.Property("ManifestId") + .HasColumnType("text") + .HasColumnName("manifest_id"); + + b.Property("Parent") + .HasColumnType("text") + .HasColumnName("parent"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text") + .HasColumnName("slug"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_hierarchy"); + + b.HasIndex("CollectionId", "CustomerId", "Canonical") + .IsUnique() + .HasDatabaseName("ix_hierarchy_collection_id_customer_id_canonical") + .HasFilter("canonical is true"); + + b.HasIndex("CustomerId", "Slug", "Parent") + .IsUnique() + .HasDatabaseName("ix_hierarchy_customer_id_slug_parent"); + + b.HasIndex("ManifestId", "CustomerId", "Canonical") + .IsUnique() + .HasDatabaseName("ix_hierarchy_manifest_id_customer_id_canonical") + .HasFilter("canonical is true"); + + b.ToTable("hierarchy", null, t => + { + t.HasCheckConstraint("stop_collection_and_manifest_in_same_record", "num_nonnulls(manifest_id, collection_id) = 1"); + }); + }); + + modelBuilder.Entity("Models.Database.General.Hierarchy", b => + { + b.HasOne("Models.Database.Collections.Collection", "Collection") + .WithMany("Hierarchy") + .HasForeignKey("CollectionId", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_hierarchy_collections_collection_id_customer_id"); + + b.HasOne("Models.Database.Collections.Manifest", "Manifest") + .WithMany("Hierarchy") + .HasForeignKey("ManifestId", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_hierarchy_manifests_manifest_id_customer_id"); + + b.Navigation("Collection"); + + b.Navigation("Manifest"); + }); + + modelBuilder.Entity("Models.Database.Collections.Collection", b => + { + b.Navigation("Hierarchy"); + }); + + modelBuilder.Entity("Models.Database.Collections.Manifest", b => + { + b.Navigation("Hierarchy"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/20241016163422_removePublicFromHierarchy.cs b/src/IIIFPresentation/Repository/Migrations/20241016163422_removePublicFromHierarchy.cs new file mode 100644 index 00000000..48c72c4e --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241016163422_removePublicFromHierarchy.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repository.Migrations +{ + /// + public partial class removePublicFromHierarchy : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "public", + table: "hierarchy"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "public", + table: "hierarchy", + type: "boolean", + nullable: false, + defaultValue: false); + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs index 7c064d5f..63815ab9 100644 --- a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs +++ b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs @@ -131,10 +131,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("parent"); - b.Property("Public") - .HasColumnType("boolean") - .HasColumnName("public"); - b.Property("Slug") .IsRequired() .HasColumnType("text") diff --git a/src/IIIFPresentation/Repository/PresentationContext.cs b/src/IIIFPresentation/Repository/PresentationContext.cs index 167f8726..eb7b5434 100644 --- a/src/IIIFPresentation/Repository/PresentationContext.cs +++ b/src/IIIFPresentation/Repository/PresentationContext.cs @@ -38,7 +38,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.Label).HasColumnType("jsonb"); - // TODO: is there issues on deletions for hierarchy with manifest/collections with the same key? entity.HasMany(e => e.Hierarchy) .WithOne(e => e.Collection) .HasForeignKey(e => new { e.CollectionId, e.CustomerId }) diff --git a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs index a9f323ad..5dd6ce7f 100644 --- a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs +++ b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs @@ -56,8 +56,7 @@ await DbContext.Hierarchy.AddAsync(new Hierarchy Slug = "", Type = ResourceType.StorageCollection, CustomerId = 1, - Canonical = true, - Public = true + Canonical = true }); await DbContext.Collections.AddAsync(new Collection @@ -85,8 +84,7 @@ await DbContext.Hierarchy.AddAsync(new Hierarchy Parent = RootCollection.Id, Type = ResourceType.StorageCollection, CustomerId = 1, - Canonical = true, - Public = true + Canonical = true }); await DbContext.Collections.AddAsync(new Collection() @@ -114,8 +112,7 @@ await DbContext.Hierarchy.AddAsync(new Hierarchy Parent = "FirstChildCollection", Type = ResourceType.StorageCollection, CustomerId = 1, - Canonical = true, - Public = true + Canonical = true }); await DbContext.Collections.AddAsync(new Collection() @@ -143,8 +140,7 @@ await DbContext.Hierarchy.AddAsync(new Hierarchy Parent = RootCollection.Id, Type = ResourceType.StorageCollection, CustomerId = 1, - Canonical = true, - Public = false + Canonical = true }); await DbContext.Collections.AddAsync(new Collection() @@ -172,8 +168,7 @@ await DbContext.Hierarchy.AddAsync(new Hierarchy Parent = RootCollection.Id, Type = ResourceType.IIIFCollection, CustomerId = 1, - Canonical = true, - Public = true + Canonical = true }); await DbContext.SaveChangesAsync(); From fb3370870a579fce28bb8f431561b9ee8936620b Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Thu, 17 Oct 2024 15:12:21 +0100 Subject: [PATCH 09/11] simplify tracked call --- .../Storage/Helpers/PresentationContextX.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs index bad93894..72ae7ecf 100644 --- a/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs +++ b/src/IIIFPresentation/API/Features/Storage/Helpers/PresentationContextX.cs @@ -53,12 +53,11 @@ public static class PresentationContextX public static async Task RetrieveCollectionAsync(this PresentationContext dbContext, int customerId, string collectionId, bool tracked = false, CancellationToken cancellationToken = default) { - var collection = tracked ? await dbContext.Collections.Include(c => c.Hierarchy) - .FirstOrDefaultAsync(s => s.CustomerId == customerId && s.Id == collectionId, cancellationToken) - : await dbContext.Collections.Include(c => c.Hierarchy).AsNoTracking() + var collection = tracked ? dbContext.Collections : dbContext.Collections.AsNoTracking(); + + return await collection.Include(c => c.Hierarchy) .FirstOrDefaultAsync(s => s.CustomerId == customerId && s.Id == collectionId, cancellationToken); - - return collection; + } /// @@ -70,12 +69,11 @@ public static class PresentationContextX /// Whether the resource should be tracked or not /// A query containing child collections public static IQueryable RetrieveCollectionItems(this PresentationContext dbContext, int customerId, - string collectionId, bool tracking = false) + string collectionId, bool tracked = false) { - return tracking ? dbContext.Collections.Include(c => c.Hierarchy).Where(c => - c.CustomerId == customerId && c.Hierarchy!.Single(h => h.Canonical).Parent == collectionId) - : dbContext.Collections.Include(c => c.Hierarchy).AsNoTracking().Where(c => - c.CustomerId == customerId && c.Hierarchy!.Single(h => h.Canonical).Parent == collectionId); + var collection = tracked ? dbContext.Collections : dbContext.Collections.AsNoTracking(); + return collection.Include(c => c.Hierarchy) + .Where(c => c.CustomerId == customerId && c.Hierarchy!.Single(h => h.Canonical).Parent == collectionId); } public static async Task GetTotalItemCountForCollection(this PresentationContext dbContext, Collection collection, From 9494b5a932574ddaa021cb4b07ba86ca03de0197 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Thu, 17 Oct 2024 15:49:24 +0100 Subject: [PATCH 10/11] fixing GitHub comments --- .../API/Features/Storage/Requests/CreateCollection.cs | 2 +- .../Features/Storage/Requests/PostHierarchicalCollection.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs index c9f20d42..fb0dcc69 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -52,7 +52,7 @@ public async Task(); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs index f995fbdd..5af81c5e 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/PostHierarchicalCollection.cs @@ -92,7 +92,7 @@ await bucketWriter.WriteToBucket( private static DatabaseCollection.Collection CreateDatabaseCollection(PostHierarchicalCollection request, Collection collectionFromBody, string id, Hierarchy parentHierarchy, string[] splitSlug) { - var thumbnails = collectionFromBody.Thumbnail?.Select(x => x as Image).ToList(); + var thumbnails = collectionFromBody.Thumbnail?.OfType().ToList(); var dateCreated = DateTime.UtcNow; var collection = new DatabaseCollection.Collection @@ -105,7 +105,7 @@ private static DatabaseCollection.Collection CreateDatabaseCollection(PostHierar IsPublic = collectionFromBody.Behavior != null && collectionFromBody.Behavior.IsPublic(), IsStorageCollection = false, Label = collectionFromBody.Label, - Thumbnail = thumbnails!?.GetThumbnailPath(), + Thumbnail = thumbnails?.GetThumbnailPath(), Hierarchy = [ new Hierarchy { From 9212e9a49099218edf430a23d3210f0ed0375500 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Fri, 18 Oct 2024 10:47:19 +0100 Subject: [PATCH 11/11] fix id to hierarchy change --- .../API/Features/Storage/Requests/GetHierarchicalCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs index 9b06d83e..22bda173 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs @@ -41,7 +41,7 @@ public async Task Handle(GetHierarchicalCollection request, if (hierarchy.Type != ResourceType.StorageCollection) { var objectFromS3 = await bucketReader.GetObjectFromBucket(new ObjectInBucket(settings.S3.StorageBucket, - collection.GetCollectionBucketKey()), cancellationToken); + hierarchy.Collection!.GetCollectionBucketKey()), cancellationToken); if (!objectFromS3.Stream.IsNull()) {