From 3bc4607bcef827553b0ebb2db7704ef0dded65e0 Mon Sep 17 00:00:00 2001 From: "jack.lewis" Date: Thu, 19 Dec 2024 14:52:26 +0000 Subject: [PATCH] initial commit adding ability to update the canvas_paintings table with asset details from the DLCS --- .../API/Helpers/PathGenerator.cs | 16 +- .../AWS/Settings/AWSSettings.cs | 4 +- .../AWS/Settings/SQSSettings.cs | 7 +- .../BatchCompletionMessageHandlerTests.cs | 6 + .../BackgroundHandler.csproj | 1 + .../BatchCompletion/BatchCompletionMessage.cs | 11 + .../BatchCompletionMessageHandler.cs | 122 ++++++ .../Infrastructure/ServiceCollectionX.cs | 11 +- .../BackgroundHandler/Program.cs | 16 +- .../DLCS/API/DlcsApiClient.cs | 21 + src/IIIFPresentation/DLCS/DlcsSettings.cs | 10 + .../DLCS/Handlers/AmbientAuthHandler.cs | 2 +- .../DLCS/Handlers/AmbientAuthLocalHandler.cs | 21 + src/IIIFPresentation/DLCS/Models/AllImages.cs | 23 + src/IIIFPresentation/DLCS/Models/Asset.cs | 10 + src/IIIFPresentation/DLCS/Models/AssetId.cs | 51 +++ .../DLCS/ServiceCollectionX.cs | 21 +- .../Models/Database/General/Batch.cs | 5 + ...41217155051_addFinishedToBatch.Designer.cs | 404 ++++++++++++++++++ .../20241217155051_addFinishedToBatch.cs | 29 ++ .../PresentationContextModelSnapshot.cs | 4 + 21 files changed, 782 insertions(+), 13 deletions(-) create mode 100644 src/IIIFPresentation/BackgroundHandler.Tests/BatchCompletion/BatchCompletionMessageHandlerTests.cs create mode 100644 src/IIIFPresentation/BackgroundHandler/BatchCompletion/BatchCompletionMessage.cs create mode 100644 src/IIIFPresentation/BackgroundHandler/BatchCompletion/BatchCompletionMessageHandler.cs create mode 100644 src/IIIFPresentation/DLCS/Handlers/AmbientAuthLocalHandler.cs create mode 100644 src/IIIFPresentation/DLCS/Models/AllImages.cs create mode 100644 src/IIIFPresentation/DLCS/Models/Asset.cs create mode 100644 src/IIIFPresentation/DLCS/Models/AssetId.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20241217155051_addFinishedToBatch.Designer.cs create mode 100644 src/IIIFPresentation/Repository/Migrations/20241217155051_addFinishedToBatch.cs diff --git a/src/IIIFPresentation/API/Helpers/PathGenerator.cs b/src/IIIFPresentation/API/Helpers/PathGenerator.cs index 58a97147..ae874b3b 100644 --- a/src/IIIFPresentation/API/Helpers/PathGenerator.cs +++ b/src/IIIFPresentation/API/Helpers/PathGenerator.cs @@ -1,5 +1,6 @@ using API.Infrastructure.Requests; using DLCS; +using DLCS.Models; using Microsoft.Extensions.Options; using Models.Database.Collections; using Models.Database.General; @@ -92,12 +93,21 @@ public string GenerateCanvasId(CanvasPainting canvasPainting) public Uri? GenerateAssetUri(CanvasPainting canvasPainting) { if (string.IsNullOrEmpty(canvasPainting.AssetId)) return null; - var assetId = canvasPainting.AssetId.Split('/'); - if (assetId.Length != 3) return null; + + AssetId assetId; + + try + { + assetId = AssetId.FromString(canvasPainting.AssetId); + } + catch // swallow error as it's not needed + { + return null; + } var uriBuilder = new UriBuilder(dlcsSettings.ApiUri) { - Path = $"/customers/{assetId[0]}/spaces/{assetId[1]}/images/{assetId[2]}", + Path = $"/customers/{assetId.Customer}/spaces/{assetId.Space}/images/{assetId.Asset}", }; return uriBuilder.Uri; } diff --git a/src/IIIFPresentation/AWS/Settings/AWSSettings.cs b/src/IIIFPresentation/AWS/Settings/AWSSettings.cs index fce00432..9c4b0fa5 100644 --- a/src/IIIFPresentation/AWS/Settings/AWSSettings.cs +++ b/src/IIIFPresentation/AWS/Settings/AWSSettings.cs @@ -2,6 +2,8 @@ public class AWSSettings { + public const string SettingsName = "AWS"; + /// /// If true, service will use LocalStack and custom ServiceUrl /// @@ -16,4 +18,4 @@ public class AWSSettings /// SQS Settings /// public SQSSettings SQS { get; set; } = new(); -} \ No newline at end of file +} diff --git a/src/IIIFPresentation/AWS/Settings/SQSSettings.cs b/src/IIIFPresentation/AWS/Settings/SQSSettings.cs index b23e5d10..20b9ec98 100644 --- a/src/IIIFPresentation/AWS/Settings/SQSSettings.cs +++ b/src/IIIFPresentation/AWS/Settings/SQSSettings.cs @@ -7,6 +7,11 @@ public class SQSSettings /// public string? CustomerCreatedQueueName { get; set; } + /// + /// Name of queue that will receive notifications when a batch is completed + /// + public string? BatchCompletionQueueName { get; set; } + /// /// The duration (in seconds) for which the call waits for a message to arrive in the queue before returning /// @@ -21,4 +26,4 @@ public class SQSSettings /// Service root for SQS. Ignored if not running LocalStack /// public string ServiceUrl { get; set; } = "http://localhost:4566/"; -} \ No newline at end of file +} diff --git a/src/IIIFPresentation/BackgroundHandler.Tests/BatchCompletion/BatchCompletionMessageHandlerTests.cs b/src/IIIFPresentation/BackgroundHandler.Tests/BatchCompletion/BatchCompletionMessageHandlerTests.cs new file mode 100644 index 00000000..a4fef2ca --- /dev/null +++ b/src/IIIFPresentation/BackgroundHandler.Tests/BatchCompletion/BatchCompletionMessageHandlerTests.cs @@ -0,0 +1,6 @@ +namespace BackgroundHandler.Tests.BatchCompletion; + +public class BatchCompletionMessageHandlerTests +{ + +} diff --git a/src/IIIFPresentation/BackgroundHandler/BackgroundHandler.csproj b/src/IIIFPresentation/BackgroundHandler/BackgroundHandler.csproj index a304dab5..67071d95 100644 --- a/src/IIIFPresentation/BackgroundHandler/BackgroundHandler.csproj +++ b/src/IIIFPresentation/BackgroundHandler/BackgroundHandler.csproj @@ -14,6 +14,7 @@ + diff --git a/src/IIIFPresentation/BackgroundHandler/BatchCompletion/BatchCompletionMessage.cs b/src/IIIFPresentation/BackgroundHandler/BatchCompletion/BatchCompletionMessage.cs new file mode 100644 index 00000000..f2247029 --- /dev/null +++ b/src/IIIFPresentation/BackgroundHandler/BatchCompletion/BatchCompletionMessage.cs @@ -0,0 +1,11 @@ +namespace BackgroundHandler.BatchCompletion; + +public record BatchCompletionMessage( + int Id, + int CustomerId, + int Total, + int Success, + int Errors, + bool Superseded, + DateTime Started, + DateTime Finished); diff --git a/src/IIIFPresentation/BackgroundHandler/BatchCompletion/BatchCompletionMessageHandler.cs b/src/IIIFPresentation/BackgroundHandler/BatchCompletion/BatchCompletionMessageHandler.cs new file mode 100644 index 00000000..af3af218 --- /dev/null +++ b/src/IIIFPresentation/BackgroundHandler/BatchCompletion/BatchCompletionMessageHandler.cs @@ -0,0 +1,122 @@ +using System.Text.Json; +using AWS.SQS; +using BackgroundHandler.Helpers; +using Core.Helpers; +using DLCS; +using DLCS.API; +using DLCS.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Models.Database.General; +using Repository; +using Batch = Models.Database.General.Batch; + +namespace BackgroundHandler.BatchCompletion; + +public class BatchCompletionMessageHandler( + PresentationContext dbContext, + IDlcsApiClient dlcsApiClient, + IOptions dlcsOptions, + ILogger logger) + : IMessageHandler +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web); + + private readonly DlcsSettings dlcsSettings = dlcsOptions.Value; + + public async Task HandleMessage(QueueMessage message, CancellationToken cancellationToken) + { + using (LogContextHelpers.SetServiceName(nameof(BatchCompletionMessageHandler))) + { + try + { + var batchCompletionMessage = DeserializeMessage(message); + + await UpdateAssetsIfRequired(batchCompletionMessage, cancellationToken); + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "Error handling batch-completion message {MessageId}", message.MessageId); + } + } + + return false; + } + + private async Task UpdateAssetsIfRequired(BatchCompletionMessage batchCompletionMessage, CancellationToken cancellationToken) + { + var batch = await dbContext.Batches.Include(b => b.Manifest) + .ThenInclude(m => m.CanvasPaintings) + .FirstOrDefaultAsync(b => b.Id == batchCompletionMessage.Id, cancellationToken); + + // batch isn't tracked by presentation, so nothing to do + if (batch == null) return; + + // Other batches haven't completed, so no point populating items until all are complete + if (await dbContext.Batches.AnyAsync(b => b.ManifestId == batch.ManifestId && b.Status != BatchStatus.Completed, + cancellationToken)) + { + return; + } + + logger.LogInformation( + "Attempting to complete assets in batch {BatchId} for customer {CustomerId} with the manifest {ManifestId}", + batch.Id, batch.CustomerId, batch.ManifestId); + + var assets = await RetrieveImages(batch, cancellationToken); + + UpdateCanvasPaintings(assets, batch); + CompleteBatch(batch); + + await dbContext.SaveChangesAsync(cancellationToken); + + logger.LogTrace("updating batch {BatchId} has been completed", batch.Id); + } + + private void CompleteBatch(Batch batch) + { + batch.Finished = DateTime.UtcNow; //todo: change to actual batch completion time? + batch.Status = BatchStatus.Completed; + } + + private void UpdateCanvasPaintings(HydraCollection assets, Batch batch) + { + if (batch.Manifest?.CanvasPaintings == null) return; + + foreach (var canvasPainting in batch.Manifest.CanvasPaintings) + { + if (canvasPainting.CanvasOriginalId == null) // Trying to figure out an asset that hasn't been updated + { + var assetId = AssetId.FromString(canvasPainting.AssetId!); + + var asset = assets.Members.FirstOrDefault(a => a.ResourceId!.Contains($"{assetId.Space}/images/{assetId.Asset}")); + if (asset == null || asset.Ingesting) continue; + + canvasPainting.CanvasOriginalId = + new Uri( + $"{dlcsSettings.OrchestratorUri}/iiif-img/{assetId.Customer}/{assetId.Space}/{assetId.Asset}/full/max/0/default.jpg"); //todo: do we need this? Supposed to be null for an asset really + canvasPainting.Thumbnail = + new Uri( + $"{dlcsSettings.OrchestratorUri}/thumbs/{assetId.Customer}/{assetId.Space}/{assetId.Asset}/100,/max/0/default.jpg"); //todo: move this to class + canvasPainting.Modified = DateTime.UtcNow; + canvasPainting.StaticHeight = asset.Height; + canvasPainting.StaticWidth = asset.Width; + } + } + } + + private async Task> RetrieveImages(Batch batch, CancellationToken cancellationToken) + { + var assetsRequest = + batch.Manifest?.CanvasPaintings?.Where(c => c.AssetId != null).Select(c => c.AssetId!).ToList() ?? []; + + return await dlcsApiClient.RetrieveAllImages(batch.CustomerId, assetsRequest, cancellationToken); + } + + private static BatchCompletionMessage DeserializeMessage(QueueMessage message) + { + var deserialized = JsonSerializer.Deserialize(message.Body, JsonSerializerOptions); + return deserialized.ThrowIfNull(nameof(deserialized)); + } +} diff --git a/src/IIIFPresentation/BackgroundHandler/Infrastructure/ServiceCollectionX.cs b/src/IIIFPresentation/BackgroundHandler/Infrastructure/ServiceCollectionX.cs index 6f436f18..a6e1568c 100644 --- a/src/IIIFPresentation/BackgroundHandler/Infrastructure/ServiceCollectionX.cs +++ b/src/IIIFPresentation/BackgroundHandler/Infrastructure/ServiceCollectionX.cs @@ -1,6 +1,7 @@ using AWS.Configuration; using AWS.Settings; using AWS.SQS; +using BackgroundHandler.BatchCompletion; using BackgroundHandler.CustomerCreation; using BackgroundHandler.Listener; using Repository; @@ -33,6 +34,14 @@ public static IServiceCollection AddBackgroundServices(this IServiceCollection s ActivatorUtilities.CreateInstance>(sp, aws.SQS.CustomerCreatedQueueName)) .AddScoped(); } + + if (!string.IsNullOrEmpty(aws.SQS.BatchCompletionQueueName)) + { + services + .AddHostedService(sp => + ActivatorUtilities.CreateInstance>(sp, aws.SQS.BatchCompletionQueueName)) + .AddScoped(); + } return services; } @@ -45,4 +54,4 @@ public static IServiceCollection AddDataAccess(this IServiceCollection services, return services .AddPresentationContext(configuration); } -} \ No newline at end of file +} diff --git a/src/IIIFPresentation/BackgroundHandler/Program.cs b/src/IIIFPresentation/BackgroundHandler/Program.cs index 331b1d8c..ef50064e 100644 --- a/src/IIIFPresentation/BackgroundHandler/Program.cs +++ b/src/IIIFPresentation/BackgroundHandler/Program.cs @@ -1,6 +1,7 @@ using AWS.Settings; using BackgroundHandler.Infrastructure; using BackgroundHandler.Settings; +using DLCS; using Serilog; var builder = WebApplication.CreateBuilder(args); @@ -15,12 +16,17 @@ builder.Services.AddOptions() .BindConfiguration(string.Empty); -var aws = builder.Configuration.GetSection("AWS").Get() ?? new AWSSettings(); +var aws = builder.Configuration.GetSection(AWSSettings.SettingsName).Get() ?? new AWSSettings(); +var dlcsSettings = builder.Configuration.GetSection(DlcsSettings.SettingsName); +var dlcs = dlcsSettings.Get()!; -builder.Services.AddAws(builder.Configuration, builder.Environment); -builder.Services.AddDataAccess(builder.Configuration); -builder.Services.AddBackgroundServices(aws); +builder.Services.AddAws(builder.Configuration, builder.Environment) + .AddDataAccess(builder.Configuration) + .AddHttpContextAccessor() + .AddDlcsClientWithLocalAuth(dlcs) + .AddBackgroundServices(aws) + .Configure(dlcsSettings); var app = builder.Build(); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/src/IIIFPresentation/DLCS/API/DlcsApiClient.cs b/src/IIIFPresentation/DLCS/API/DlcsApiClient.cs index 3e263321..1cf9f547 100644 --- a/src/IIIFPresentation/DLCS/API/DlcsApiClient.cs +++ b/src/IIIFPresentation/DLCS/API/DlcsApiClient.cs @@ -6,6 +6,7 @@ using DLCS.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; namespace DLCS.API; @@ -26,6 +27,12 @@ public interface IDlcsApiClient /// public Task> IngestAssets(int customerId, List images, CancellationToken cancellationToken = default); + + /// + /// Retrieve a list of assets from the DLCS + /// + public Task> RetrieveAllImages(int customerId, List assets, + CancellationToken cancellationToken = default); } /// @@ -85,6 +92,20 @@ public async Task> IngestAssets(int customerId, List assets, C return batches.ToList(); } + public async Task> RetrieveAllImages(int customerId, List assets, + CancellationToken cancellationToken = default) + { + logger.LogTrace("performing an all images call for customer {CustomerId}", customerId); + var allImagesPath = $"/customers/{customerId}/allImages"; + + var allImagesRequest = new AllImages(assets); + + var asset = + await CallDlcsApi>(HttpMethod.Post, allImagesPath, allImagesRequest, cancellationToken); + + return asset ?? throw new DlcsException("Failed to retrieve all images"); + } + private async Task CallDlcsApi(HttpMethod httpMethod, string path, object payload, CancellationToken cancellationToken) { diff --git a/src/IIIFPresentation/DLCS/DlcsSettings.cs b/src/IIIFPresentation/DLCS/DlcsSettings.cs index f571229d..bf84f466 100644 --- a/src/IIIFPresentation/DLCS/DlcsSettings.cs +++ b/src/IIIFPresentation/DLCS/DlcsSettings.cs @@ -8,6 +8,11 @@ public class DlcsSettings /// URL root of DLCS API /// public required Uri ApiUri { get; set; } + + /// + /// URL root of DLCS API + /// + public Uri? OrchestratorUri { get; set; } /// /// Default timeout (in ms) use for HttpClient.Timeout. @@ -18,4 +23,9 @@ public class DlcsSettings /// The maximum size of an individual batch request /// public int MaxBatchSize { get; set; } = 100; + + /// + /// Used to authenticate requests that do not go via the HttpContextAccessor + /// + public string ApiLocalAuth { get; set; } //Todo: is this right? is there a better way of setting auth } diff --git a/src/IIIFPresentation/DLCS/Handlers/AmbientAuthHandler.cs b/src/IIIFPresentation/DLCS/Handlers/AmbientAuthHandler.cs index a0adf678..b3454bb2 100644 --- a/src/IIIFPresentation/DLCS/Handlers/AmbientAuthHandler.cs +++ b/src/IIIFPresentation/DLCS/Handlers/AmbientAuthHandler.cs @@ -25,4 +25,4 @@ protected override Task SendAsync(HttpRequestMessage reques request.Headers.Authorization = authHeader; return base.SendAsync(request, cancellationToken); } -} \ No newline at end of file +} diff --git a/src/IIIFPresentation/DLCS/Handlers/AmbientAuthLocalHandler.cs b/src/IIIFPresentation/DLCS/Handlers/AmbientAuthLocalHandler.cs new file mode 100644 index 00000000..afe38b8e --- /dev/null +++ b/src/IIIFPresentation/DLCS/Handlers/AmbientAuthLocalHandler.cs @@ -0,0 +1,21 @@ +using System.Net.Http.Headers; +using Microsoft.Extensions.Options; + +namespace DLCS.Handlers; + +public class AmbientAuthLocalHandler() : DelegatingHandler +{ + private readonly DlcsSettings dlcsSettings; + + public AmbientAuthLocalHandler(IOptions dlcsOptions) : this() + { + dlcsSettings = dlcsOptions.Value; + } + + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", dlcsSettings.ApiLocalAuth); + return base.SendAsync(request, cancellationToken); + } +} diff --git a/src/IIIFPresentation/DLCS/Models/AllImages.cs b/src/IIIFPresentation/DLCS/Models/AllImages.cs new file mode 100644 index 00000000..c9da21fd --- /dev/null +++ b/src/IIIFPresentation/DLCS/Models/AllImages.cs @@ -0,0 +1,23 @@ +using System.Text.Json.Serialization; + +namespace DLCS.Models; + +public class AllImages : JsonLdBase +{ + public AllImages(List members) + { + Members = members.Select(m => new AllImagesMember(m)).ToList(); + Type = "Collection" ; + } + + [JsonPropertyOrder(3)] + [JsonPropertyName("member")] + public List Members { get; set; } +} + +public class AllImagesMember(string id) +{ + public string Id { get; set; } = id; +} + + diff --git a/src/IIIFPresentation/DLCS/Models/Asset.cs b/src/IIIFPresentation/DLCS/Models/Asset.cs new file mode 100644 index 00000000..86d13dd3 --- /dev/null +++ b/src/IIIFPresentation/DLCS/Models/Asset.cs @@ -0,0 +1,10 @@ +namespace DLCS.Models; + +public class Asset : JsonLdBase +{ + public int? Width { get; set; } + + public int? Height { get; set; } + + public bool Ingesting { get; set; } +} diff --git a/src/IIIFPresentation/DLCS/Models/AssetId.cs b/src/IIIFPresentation/DLCS/Models/AssetId.cs new file mode 100644 index 00000000..5b150e2e --- /dev/null +++ b/src/IIIFPresentation/DLCS/Models/AssetId.cs @@ -0,0 +1,51 @@ +namespace DLCS.Models; + + +/// +/// A record that represents an identifier for a DLCS Asset. +/// +public class AssetId +{ + /// Id of customer + public int Customer { get; } + + /// Id of space + public int Space { get; } + + /// Id of asset + public string Asset { get; } + + /// + /// A record that represents an identifier for a DLCS Asset. + /// + public AssetId(int customer, int space, string asset) + { + Customer = customer; + Space = space; + Asset = asset; + } + + public override string ToString() => $"{Customer}/{Space}/{Asset}"; + + /// + /// Create a new AssetId from string in format customer/space/image + /// + public static AssetId FromString(string assetImageId) + { + var parts = assetImageId.Split("/", StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 3) + { + throw new ArgumentException($"AssetId '{assetImageId}' is invalid. Must be in format customer/space/asset"); + } + + try + { + return new AssetId(int.Parse(parts[0]), int.Parse(parts[1]), parts[2]); + } + catch (FormatException fmEx) + { + throw new ArgumentException($"AssetId '{assetImageId}' is invalid. Must be in format customer/space/asset", + fmEx); + } + } +} diff --git a/src/IIIFPresentation/DLCS/ServiceCollectionX.cs b/src/IIIFPresentation/DLCS/ServiceCollectionX.cs index cda11aab..629eb5e6 100644 --- a/src/IIIFPresentation/DLCS/ServiceCollectionX.cs +++ b/src/IIIFPresentation/DLCS/ServiceCollectionX.cs @@ -27,4 +27,23 @@ public static IServiceCollection AddDlcsClient(this IServiceCollection services, return services; } -} \ No newline at end of file + + /// + /// Add to services collection. + /// + public static IServiceCollection AddDlcsClientWithLocalAuth(this IServiceCollection services, + DlcsSettings dlcsSettings) + { + services + .AddScoped() + .AddTransient() + .AddHttpClient(client => + { + client.BaseAddress = dlcsSettings.ApiUri; + client.Timeout = TimeSpan.FromMilliseconds(dlcsSettings.DefaultTimeoutMs); + }).AddHttpMessageHandler() + .AddHttpMessageHandler(); + + return services; + } +} diff --git a/src/IIIFPresentation/Models/Database/General/Batch.cs b/src/IIIFPresentation/Models/Database/General/Batch.cs index 8187dc1f..c897e1ef 100644 --- a/src/IIIFPresentation/Models/Database/General/Batch.cs +++ b/src/IIIFPresentation/Models/Database/General/Batch.cs @@ -24,6 +24,11 @@ public class Batch /// public DateTime Submitted { get; set; } + /// + /// When the batch was added to the DLCS + /// + public DateTime? Finished { get; set; } + /// /// Id of related manifest /// diff --git a/src/IIIFPresentation/Repository/Migrations/20241217155051_addFinishedToBatch.Designer.cs b/src/IIIFPresentation/Repository/Migrations/20241217155051_addFinishedToBatch.Designer.cs new file mode 100644 index 00000000..3edc62e3 --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241217155051_addFinishedToBatch.Designer.cs @@ -0,0 +1,404 @@ +// +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("20241217155051_addFinishedToBatch")] + partial class addFinishedToBatch + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "citext"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Models.Database.CanvasPainting", b => + { + b.Property("CanvasPaintingId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("canvas_painting_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("CanvasPaintingId")); + + b.Property("AssetId") + .HasColumnType("text") + .HasColumnName("asset_id"); + + b.Property("CanvasLabel") + .HasColumnType("text") + .HasColumnName("canvas_label"); + + b.Property("CanvasOrder") + .HasColumnType("integer") + .HasColumnName("canvas_order"); + + b.Property("CanvasOriginalId") + .HasColumnType("text") + .HasColumnName("canvas_original_id"); + + b.Property("ChoiceOrder") + .IsRequired() + .HasColumnType("integer") + .HasColumnName("choice_order"); + + b.Property("Created") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created") + .HasDefaultValueSql("now()"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("Id") + .HasColumnType("text") + .HasColumnName("canvas_id"); + + b.Property("Label") + .HasColumnType("jsonb") + .HasColumnName("label"); + + b.Property("ManifestId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("manifest_id"); + + b.Property("Modified") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("modified") + .HasDefaultValueSql("now()"); + + b.Property("StaticHeight") + .HasColumnType("integer") + .HasColumnName("static_height"); + + b.Property("StaticWidth") + .HasColumnType("integer") + .HasColumnName("static_width"); + + b.Property("Target") + .HasColumnType("text") + .HasColumnName("target"); + + b.Property("Thumbnail") + .HasColumnType("text") + .HasColumnName("thumbnail"); + + b.HasKey("CanvasPaintingId") + .HasName("pk_canvas_paintings"); + + b.HasIndex("ManifestId", "CustomerId") + .HasDatabaseName("ix_canvas_paintings_manifest_id_customer_id"); + + b.HasIndex("Id", "CustomerId", "ManifestId", "AssetId", "CanvasOrder", "ChoiceOrder") + .IsUnique() + .HasDatabaseName("ix_canvas_paintings_canvas_id_customer_id_manifest_id_asset_id") + .HasFilter("canvas_original_id is null"); + + b.HasIndex("Id", "CustomerId", "ManifestId", "CanvasOriginalId", "CanvasOrder", "ChoiceOrder") + .IsUnique() + .HasDatabaseName("ix_canvas_paintings_canvas_id_customer_id_manifest_id_canvas_o") + .HasFilter("asset_id is null"); + + b.ToTable("canvas_paintings", (string)null); + }); + + 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.Property("Created") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created") + .HasDefaultValueSql("now()"); + + b.Property("CreatedBy") + .HasColumnType("text") + .HasColumnName("created_by"); + + b.Property("Label") + .HasColumnType("text") + .HasColumnName("label"); + + b.Property("Modified") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("modified") + .HasDefaultValueSql("now()"); + + b.Property("ModifiedBy") + .HasColumnType("text") + .HasColumnName("modified_by"); + + b.Property("SpaceId") + .HasColumnType("integer") + .HasColumnName("space_id"); + + b.HasKey("Id", "CustomerId") + .HasName("pk_manifests"); + + b.ToTable("manifests", (string)null); + }); + + modelBuilder.Entity("Models.Database.General.Batch", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customer_id"); + + b.Property("Finished") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished"); + + b.Property("ManifestId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("manifest_id"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("Submitted") + .HasColumnType("timestamp with time zone") + .HasColumnName("submitted"); + + b.HasKey("Id") + .HasName("pk_batches"); + + b.HasIndex("ManifestId", "CustomerId") + .HasDatabaseName("ix_batches_manifest_id_customer_id"); + + b.ToTable("batches", (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("citext") + .HasColumnName("slug"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_hierarchy"); + + b.HasIndex("Parent", "CustomerId") + .HasDatabaseName("ix_hierarchy_parent_customer_id"); + + 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.CanvasPainting", b => + { + b.HasOne("Models.Database.Collections.Manifest", "Manifest") + .WithMany("CanvasPaintings") + .HasForeignKey("ManifestId", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_canvas_paintings_manifests_manifest_id_customer_id"); + + b.Navigation("Manifest"); + }); + + modelBuilder.Entity("Models.Database.General.Batch", b => + { + b.HasOne("Models.Database.Collections.Manifest", "Manifest") + .WithMany("Batches") + .HasForeignKey("ManifestId", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_batches_manifests_manifest_id_customer_id"); + + b.Navigation("Manifest"); + }); + + 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.HasOne("Models.Database.Collections.Collection", "ParentCollection") + .WithMany("Children") + .HasForeignKey("Parent", "CustomerId") + .OnDelete(DeleteBehavior.NoAction) + .HasConstraintName("fk_hierarchy_collections_parent_customer_id"); + + b.Navigation("Collection"); + + b.Navigation("Manifest"); + + b.Navigation("ParentCollection"); + }); + + modelBuilder.Entity("Models.Database.Collections.Collection", b => + { + b.Navigation("Children"); + + b.Navigation("Hierarchy"); + }); + + modelBuilder.Entity("Models.Database.Collections.Manifest", b => + { + b.Navigation("Batches"); + + b.Navigation("CanvasPaintings"); + + b.Navigation("Hierarchy"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/20241217155051_addFinishedToBatch.cs b/src/IIIFPresentation/Repository/Migrations/20241217155051_addFinishedToBatch.cs new file mode 100644 index 00000000..1250c059 --- /dev/null +++ b/src/IIIFPresentation/Repository/Migrations/20241217155051_addFinishedToBatch.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Repository.Migrations +{ + /// + public partial class addFinishedToBatch : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "finished", + table: "batches", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "finished", + table: "batches"); + } + } +} diff --git a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs index 02ebdfb8..c5c48d5b 100644 --- a/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs +++ b/src/IIIFPresentation/Repository/Migrations/PresentationContextModelSnapshot.cs @@ -234,6 +234,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer") .HasColumnName("customer_id"); + b.Property("Finished") + .HasColumnType("timestamp with time zone") + .HasColumnName("finished"); + b.Property("ManifestId") .IsRequired() .HasColumnType("text")