From 6e5755039fae605ba74df7f9aa6a947a2473c653 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 5 Jul 2026 14:49:33 +0200 Subject: [PATCH] feat(infra): track saga failure handling + cleanup (phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On definitive failure of the picture or audio slip, the saga now reaches a Failed state and recovers end-to-end: - Picture/audio track slips carry TrackId + ProcessKind variables and a subscription to a fault translator (TrackProcessingSlipFaultConsumer) that turns RoutingSlipFaulted into a typed Track{Picture,Audio}ProcessingFailed - The saga stores the final object keys from the start event and, on failure, transitions to Failed and publishes TrackProcessingFailed - TrackProcessingFailedConsumer runs a cleanup slip that removes the orphaned final picture/audio copies and marks the track LifeCycleStatus=Failed (MarkTrackAsFailedActivity) — the track stays as a visible, recoverable record - Migration for the new saga columns Verified: Worker boots against RabbitMQ + Postgres in Docker; the new endpoints (fault translator, failed consumer, mark-track-as-failed) configure and the bus starts clean. Build: 0 warnings, 0 errors. --- .../Events/TrackProcessingFailed.cs | 8 + .../Tracks/MarkTrackAsFailedActivity.cs | 32 + .../Arguments/MarkTrackAsFailedArguments.cs | 4 + .../TrackProcessingFailedConsumer.cs | 42 ++ .../TrackProcessingSlipFaultConsumer.cs | 33 + ...TransitDependencyInjection.Registration.cs | 3 + .../MassTransit/RoutingSlip/ActivityNames.cs | 1 + .../AudioWorkflowRoutingSlipBuilder.cs | 6 + .../PictureWorkflowRoutingSlipBuilder.cs | 6 + .../RoutingSlip/RoutingSlipVariableNames.cs | 8 + .../MassTransit/Sagas/TrackProcessingState.cs | 6 + .../Sagas/TrackProcessingStateMachine.cs | 17 +- ...807_AddTrackProcessingSagaKeys.Designer.cs | 619 ++++++++++++++++++ ...260705124807_AddTrackProcessingSagaKeys.cs | 48 ++ .../Migrations/DatabaseModelSnapshot.cs | 9 + 15 files changed, 840 insertions(+), 2 deletions(-) create mode 100644 backend/Musify.Application/Events/TrackProcessingFailed.cs create mode 100644 backend/Musify.Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsFailedActivity.cs create mode 100644 backend/Musify.Infrastructure/MassTransit/Arguments/MarkTrackAsFailedArguments.cs create mode 100644 backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingFailedConsumer.cs create mode 100644 backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingSlipFaultConsumer.cs create mode 100644 backend/Musify.Infrastructure/Persistence/Migrations/20260705124807_AddTrackProcessingSagaKeys.Designer.cs create mode 100644 backend/Musify.Infrastructure/Persistence/Migrations/20260705124807_AddTrackProcessingSagaKeys.cs diff --git a/backend/Musify.Application/Events/TrackProcessingFailed.cs b/backend/Musify.Application/Events/TrackProcessingFailed.cs new file mode 100644 index 0000000..5234fe6 --- /dev/null +++ b/backend/Musify.Application/Events/TrackProcessingFailed.cs @@ -0,0 +1,8 @@ +namespace Musify.Application.Events +{ + public record TrackProcessingFailed( + Guid TrackId, + string? Bucket, + string? PictureKey, + string? AudioKey); +} diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsFailedActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsFailedActivity.cs new file mode 100644 index 0000000..10541b1 --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Tracks/MarkTrackAsFailedActivity.cs @@ -0,0 +1,32 @@ +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts; +using Musify.Domain.ValueObjects; +using Musify.Infrastructure.MassTransit.Arguments; + +namespace Musify.Infrastructure.MassTransit.Activities.Tracks +{ + internal class MarkTrackAsFailedActivity( + IDatabase database, + ILogger logger) + : IExecuteActivity + { + public const string ExecuteEndpointName = "mark-track-as-failed"; + + public async Task Execute(ExecuteContext executeContext) + { + var track = await database.Tracks + .SingleOrDefaultAsync(t => t.Id == executeContext.Arguments.TrackId, executeContext.CancellationToken); + + if (track is not null) + { + track.LifeCycleStatus = LifeCycleStatus.Failed; + await database.SaveChangesAsync(executeContext.CancellationToken); + logger.LogInformation("Marked track {TrackId} as failed", executeContext.Arguments.TrackId); + } + + return executeContext.Completed(); + } + } +} diff --git a/backend/Musify.Infrastructure/MassTransit/Arguments/MarkTrackAsFailedArguments.cs b/backend/Musify.Infrastructure/MassTransit/Arguments/MarkTrackAsFailedArguments.cs new file mode 100644 index 0000000..659f9e0 --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Arguments/MarkTrackAsFailedArguments.cs @@ -0,0 +1,4 @@ +namespace Musify.Infrastructure.MassTransit.Arguments +{ + public record MarkTrackAsFailedArguments(Guid TrackId); +} diff --git a/backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingFailedConsumer.cs b/backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingFailedConsumer.cs new file mode 100644 index 0000000..07bdb55 --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingFailedConsumer.cs @@ -0,0 +1,42 @@ +using MassTransit; +using MassTransit.Courier.Contracts; +using Musify.Application.Events; +using Musify.Infrastructure.MassTransit.Activities.Files; +using Musify.Infrastructure.MassTransit.Activities.Tracks; +using Musify.Infrastructure.MassTransit.Arguments; +using Musify.Infrastructure.MassTransit.RoutingSlip; + +namespace Musify.Infrastructure.MassTransit.Consumers +{ + public class TrackProcessingFailedConsumer(IBus bus) : IConsumer + { + public async Task Consume(ConsumeContext context) + { + var message = context.Message; + + var builder = new RoutingSlipBuilder(NewId.NextGuid()); + builder.AddSubscription( + EndpointHelper.BuildConsumerUri(RoutingSlipCleanUpConsumer.QueueName), + RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + + if (!string.IsNullOrWhiteSpace(message.Bucket) && !string.IsNullOrWhiteSpace(message.PictureKey)) + builder.AddActivity( + ActivityNames.RemoveTrackOriginalPicture, + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments(message.Bucket, message.PictureKey)); + + if (!string.IsNullOrWhiteSpace(message.Bucket) && !string.IsNullOrWhiteSpace(message.AudioKey)) + builder.AddActivity( + ActivityNames.RemoveTrackOriginalAudio, + EndpointHelper.BuildExecuteActivityUri(RemoveFileFromBucketActivity.ExecuteEndpointName), + new RemoveFileFromBucketArguments(message.Bucket, message.AudioKey)); + + builder.AddActivity( + ActivityNames.MarkTrackAsFailed, + EndpointHelper.BuildExecuteActivityUri(MarkTrackAsFailedActivity.ExecuteEndpointName), + new MarkTrackAsFailedArguments(message.TrackId)); + + await bus.Execute(builder.Build()); + } + } +} diff --git a/backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingSlipFaultConsumer.cs b/backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingSlipFaultConsumer.cs new file mode 100644 index 0000000..edab8bb --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Consumers/TrackProcessingSlipFaultConsumer.cs @@ -0,0 +1,33 @@ +using MassTransit; +using MassTransit.Courier.Contracts; +using Musify.Application.Events; + +namespace Musify.Infrastructure.MassTransit.Consumers +{ + public class TrackProcessingSlipFaultConsumer : IConsumer + { + public const string QueueName = "track-processing-slip-fault"; + + public async Task Consume(ConsumeContext context) + { + var variables = context.Message.Variables; + + if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.ProcessKind, out var kindObject)) + return; + + if (!variables.TryGetValue(RoutingSlipVariableNames.Workflow.TrackId, out var trackObject) + || !Guid.TryParse(trackObject?.ToString(), out var trackId)) + return; + + switch (kindObject?.ToString()) + { + case RoutingSlipVariableNames.ProcessKinds.TrackPicture: + await context.Publish(new TrackPictureProcessingFailed(trackId)); + break; + case RoutingSlipVariableNames.ProcessKinds.TrackAudio: + await context.Publish(new TrackAudioProcessingFailed(trackId)); + break; + } + } + } +} diff --git a/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs b/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs index fc9b54f..820aadc 100644 --- a/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs +++ b/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.Registration.cs @@ -36,6 +36,8 @@ namespace Musify.Infrastructure.MassTransit options.AddConsumer(); options.AddConsumer(); options.AddConsumer(); + options.AddConsumer(); + options.AddConsumer(); options.AddExecuteActivity(); options.AddExecuteActivity(); @@ -46,6 +48,7 @@ namespace Musify.Infrastructure.MassTransit options.AddExecuteActivity(); options.AddExecuteActivity(); options.AddExecuteActivity(); + options.AddExecuteActivity(); options.AddActivity(); options.AddActivity(); diff --git a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs index 3767c6c..4c1a912 100644 --- a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs +++ b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/ActivityNames.cs @@ -3,6 +3,7 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip internal static class ActivityNames { internal const string MarkTrackAsRemoving = "MarkTrackAsRemoving"; + internal const string MarkTrackAsFailed = "MarkTrackAsFailed"; internal const string DeleteTrackFromDb = "DeleteTrackFromDb"; internal const string MarkPlayListAsRemoving = "MarkPlayListAsRemoving"; internal const string DeletePlayListFromDb = "DeletePlayListFromDb"; diff --git a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs index cbf30fe..9016fab 100644 --- a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs +++ b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/AudioWorkflowRoutingSlipBuilder.cs @@ -60,6 +60,12 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders message.TrackId, message.DestinationFolderKey)); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.TrackId, message.TrackId); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.ProcessKind, RoutingSlipVariableNames.ProcessKinds.TrackAudio); + routingSlipBuilder.AddSubscription( + EndpointHelper.BuildConsumerUri(TrackProcessingSlipFaultConsumer.QueueName), + RoutingSlipEvents.Faulted); + return routingSlipBuilder; } } diff --git a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs index 71bc2ae..731d160 100644 --- a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs +++ b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/Builders/PictureWorkflowRoutingSlipBuilder.cs @@ -32,6 +32,12 @@ namespace Musify.Infrastructure.MassTransit.RoutingSlip.Builders RoutingSlipVariableNames.Picture.MediumResizedFilePath, RoutingSlipVariableNames.Picture.LargeResizedFilePath)); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.TrackId, message.TrackId); + routingSlipBuilder.AddVariable(RoutingSlipVariableNames.Workflow.ProcessKind, RoutingSlipVariableNames.ProcessKinds.TrackPicture); + routingSlipBuilder.AddSubscription( + EndpointHelper.BuildConsumerUri(TrackProcessingSlipFaultConsumer.QueueName), + RoutingSlipEvents.Faulted); + return routingSlipBuilder; } diff --git a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/RoutingSlipVariableNames.cs b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/RoutingSlipVariableNames.cs index 2667a18..de4fdb6 100644 --- a/backend/Musify.Infrastructure/MassTransit/RoutingSlip/RoutingSlipVariableNames.cs +++ b/backend/Musify.Infrastructure/MassTransit/RoutingSlip/RoutingSlipVariableNames.cs @@ -6,6 +6,14 @@ namespace Musify.Infrastructure.MassTransit { internal const string TemporalDirectory = "Workflow.TemporalDirectory"; internal const string CorrelationId = "Workflow.CorrelationId"; + internal const string TrackId = "Workflow.TrackId"; + internal const string ProcessKind = "Workflow.ProcessKind"; + } + + internal static class ProcessKinds + { + internal const string TrackPicture = "TrackPicture"; + internal const string TrackAudio = "TrackAudio"; } internal static class Audio diff --git a/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingState.cs b/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingState.cs index 4d37377..707a3d6 100644 --- a/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingState.cs +++ b/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingState.cs @@ -12,6 +12,12 @@ namespace Musify.Infrastructure.MassTransit.Sagas public bool AudioProcessed { get; set; } + public string? Bucket { get; set; } + + public string? PictureKey { get; set; } + + public string? AudioKey { get; set; } + public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } diff --git a/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingStateMachine.cs b/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingStateMachine.cs index 4b73233..faba2e3 100644 --- a/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingStateMachine.cs +++ b/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingStateMachine.cs @@ -30,6 +30,9 @@ namespace Musify.Infrastructure.MassTransit.Sagas { context.Saga.CreatedAt = DateTime.UtcNow; context.Saga.UpdatedAt = DateTime.UtcNow; + context.Saga.Bucket = context.Message.Bucket; + context.Saga.PictureKey = context.Message.PictureDestinationKey; + context.Saga.AudioKey = context.Message.AudioDestinationKey; }) .TransitionTo(Processing)); @@ -50,10 +53,20 @@ namespace Musify.Infrastructure.MassTransit.Sagas .If(context => context.Saga.PictureProcessed, binder => binder.Finalize()), When(PictureFailed) .Then(context => context.Saga.UpdatedAt = DateTime.UtcNow) - .TransitionTo(Failed), + .TransitionTo(Failed) + .Publish(context => new TrackProcessingFailed( + context.Saga.CorrelationId, + context.Saga.Bucket, + context.Saga.PictureKey, + context.Saga.AudioKey)), When(AudioFailed) .Then(context => context.Saga.UpdatedAt = DateTime.UtcNow) - .TransitionTo(Failed)); + .TransitionTo(Failed) + .Publish(context => new TrackProcessingFailed( + context.Saga.CorrelationId, + context.Saga.Bucket, + context.Saga.PictureKey, + context.Saga.AudioKey))); During(Failed, Ignore(PictureProcessed), diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/20260705124807_AddTrackProcessingSagaKeys.Designer.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260705124807_AddTrackProcessingSagaKeys.Designer.cs new file mode 100644 index 0000000..84db2e7 --- /dev/null +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260705124807_AddTrackProcessingSagaKeys.Designer.cs @@ -0,0 +1,619 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260705124807_AddTrackProcessingSagaKeys")] + partial class AddTrackProcessingSagaKeys + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LargePictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SmallPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudioFolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AudioTranscodeProcessingStatus") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LargePictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LastRetryAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("MediumPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OriginalAudioName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("OriginalPictureName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PicturesProcessingStatus") + .HasColumnType("integer"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("SmallPictureName") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("ProfilePictureUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("AudioKey") + .HasColumnType("text"); + + b.Property("AudioProcessed") + .HasColumnType("boolean"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("PictureProcessed") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("TrackProcessingState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/20260705124807_AddTrackProcessingSagaKeys.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260705124807_AddTrackProcessingSagaKeys.cs new file mode 100644 index 0000000..f1fe6c5 --- /dev/null +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260705124807_AddTrackProcessingSagaKeys.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddTrackProcessingSagaKeys : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AudioKey", + table: "TrackProcessingState", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Bucket", + table: "TrackProcessingState", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "PictureKey", + table: "TrackProcessingState", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AudioKey", + table: "TrackProcessingState"); + + migrationBuilder.DropColumn( + name: "Bucket", + table: "TrackProcessingState"); + + migrationBuilder.DropColumn( + name: "PictureKey", + table: "TrackProcessingState"); + } + } +} diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index da9d2fe..71a9d9f 100644 --- a/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -489,9 +489,15 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("CorrelationId") .HasColumnType("uuid"); + b.Property("AudioKey") + .HasColumnType("text"); + b.Property("AudioProcessed") .HasColumnType("boolean"); + b.Property("Bucket") + .HasColumnType("text"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); @@ -500,6 +506,9 @@ namespace Musify.Infrastructure.Persistence.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("PictureKey") + .HasColumnType("text"); + b.Property("PictureProcessed") .HasColumnType("boolean");