From 986941166c9a172bd305ee9b04395b0bfc9476c8 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Sun, 5 Jul 2026 14:41:19 +0200 Subject: [PATCH] feat(infra): add Track processing saga (state machine, phase 1) Introduce a MassTransit saga state machine that correlates by TrackId and tracks the two parallel processing slips (picture + audio): - TrackProcessingStateMachine + EF-persisted TrackProcessingState (pessimistic concurrency, EntityFrameworkRepository), migration added - Starts on CreateTrackResourcesEvent; UpdateTrack{Picture,Audio}Activity now publish TrackPictureProcessed / TrackAudioProcessed on success; when both arrive the saga finalizes (Ready). Failure transitions to a Failed state (cleanup wired in the next phase) - Add LifeCycleStatus.Failed and the four outcome events Verified: booted the Worker against RabbitMQ + migrated Postgres in Docker; the saga endpoint 'track-processing-state' is configured and the bus starts clean. Build: 0 warnings, 0 errors. Co-Authored-By: Claude Opus 4.8 --- .../Events/TrackAudioProcessed.cs | 4 + .../Events/TrackAudioProcessingFailed.cs | 4 + .../Events/TrackPictureProcessed.cs | 4 + .../Events/TrackPictureProcessingFailed.cs | 4 + .../ValueObjects/LifeCycleStatus.cs | 3 +- .../Audio/UpdateTrackAudioActivity.cs | 9 + .../Pictures/UpdateTrackPictureActivity.cs | 6 + .../MassTransitDependencyInjection.cs | 9 + .../MassTransit/Sagas/TrackProcessingState.cs | 19 + .../Sagas/TrackProcessingStateMachine.cs | 67 ++ .../Persistence/Database.cs | 6 + ...32_AddTrackProcessingStateSaga.Designer.cs | 610 ++++++++++++++++++ ...60705123932_AddTrackProcessingStateSaga.cs | 38 ++ .../Migrations/DatabaseModelSnapshot.cs | 27 + 14 files changed, 809 insertions(+), 1 deletion(-) create mode 100644 backend/Musify.Application/Events/TrackAudioProcessed.cs create mode 100644 backend/Musify.Application/Events/TrackAudioProcessingFailed.cs create mode 100644 backend/Musify.Application/Events/TrackPictureProcessed.cs create mode 100644 backend/Musify.Application/Events/TrackPictureProcessingFailed.cs create mode 100644 backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingState.cs create mode 100644 backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingStateMachine.cs create mode 100644 backend/Musify.Infrastructure/Persistence/Migrations/20260705123932_AddTrackProcessingStateSaga.Designer.cs create mode 100644 backend/Musify.Infrastructure/Persistence/Migrations/20260705123932_AddTrackProcessingStateSaga.cs diff --git a/backend/Musify.Application/Events/TrackAudioProcessed.cs b/backend/Musify.Application/Events/TrackAudioProcessed.cs new file mode 100644 index 0000000..9648bf2 --- /dev/null +++ b/backend/Musify.Application/Events/TrackAudioProcessed.cs @@ -0,0 +1,4 @@ +namespace Musify.Application.Events +{ + public record TrackAudioProcessed(Guid TrackId); +} diff --git a/backend/Musify.Application/Events/TrackAudioProcessingFailed.cs b/backend/Musify.Application/Events/TrackAudioProcessingFailed.cs new file mode 100644 index 0000000..c531163 --- /dev/null +++ b/backend/Musify.Application/Events/TrackAudioProcessingFailed.cs @@ -0,0 +1,4 @@ +namespace Musify.Application.Events +{ + public record TrackAudioProcessingFailed(Guid TrackId); +} diff --git a/backend/Musify.Application/Events/TrackPictureProcessed.cs b/backend/Musify.Application/Events/TrackPictureProcessed.cs new file mode 100644 index 0000000..e42b30f --- /dev/null +++ b/backend/Musify.Application/Events/TrackPictureProcessed.cs @@ -0,0 +1,4 @@ +namespace Musify.Application.Events +{ + public record TrackPictureProcessed(Guid TrackId); +} diff --git a/backend/Musify.Application/Events/TrackPictureProcessingFailed.cs b/backend/Musify.Application/Events/TrackPictureProcessingFailed.cs new file mode 100644 index 0000000..df08dec --- /dev/null +++ b/backend/Musify.Application/Events/TrackPictureProcessingFailed.cs @@ -0,0 +1,4 @@ +namespace Musify.Application.Events +{ + public record TrackPictureProcessingFailed(Guid TrackId); +} diff --git a/backend/Musify.Domain/ValueObjects/LifeCycleStatus.cs b/backend/Musify.Domain/ValueObjects/LifeCycleStatus.cs index bed197e..4e21417 100644 --- a/backend/Musify.Domain/ValueObjects/LifeCycleStatus.cs +++ b/backend/Musify.Domain/ValueObjects/LifeCycleStatus.cs @@ -3,6 +3,7 @@ namespace Musify.Domain.ValueObjects public enum LifeCycleStatus { Active = 0, - Removing = 1 + Removing = 1, + Failed = 2 } } diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs index befa597..499ee0e 100644 --- a/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Audio/UpdateTrackAudioActivity.cs @@ -2,6 +2,7 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Contracts; +using Musify.Application.Events; using Musify.Domain.Entities; using Musify.Domain.ValueObjects; using Musify.Infrastructure.MassTransit.Arguments; @@ -11,6 +12,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio { internal class UpdateTrackAudioActivity( IDatabase database, + IPublishEndpoint publishEndpoint, ILogger logger) : IActivity { @@ -37,6 +39,9 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio if (string.IsNullOrWhiteSpace(audioFolderKey)) { logger.LogWarning("Audio folder key is empty for track {TrackId}; skipping update", track.Id); + await publishEndpoint.Publish( + new TrackAudioProcessed(executeContext.Arguments.TrackId), + executeContext.CancellationToken); return executeContext.Completed(); } @@ -52,6 +57,10 @@ namespace Musify.Infrastructure.MassTransit.Activities.Audio logger.LogInformation("Updated track {TrackId} audio", executeContext.Arguments.TrackId); + await publishEndpoint.Publish( + new TrackAudioProcessed(executeContext.Arguments.TrackId), + executeContext.CancellationToken); + return executeContext.Completed(log); } catch (Exception exception) diff --git a/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs b/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs index 932d5a1..068b44b 100644 --- a/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs +++ b/backend/Musify.Infrastructure/MassTransit/Activities/Pictures/UpdateTrackPictureActivity.cs @@ -1,6 +1,7 @@ using MassTransit; using Microsoft.Extensions.Logging; using Musify.Application.Contracts; +using Musify.Application.Events; using Musify.Domain.ValueObjects; using Musify.Infrastructure.MassTransit.Arguments; using Musify.Infrastructure.MassTransit.Logs; @@ -9,6 +10,7 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures { internal class UpdateTrackPictureActivity( IDatabase database, + IPublishEndpoint publishEndpoint, ILogger logger) : IActivity { @@ -55,6 +57,10 @@ namespace Musify.Infrastructure.MassTransit.Activities.Pictures logger.LogInformation("Updated track {TrackId} pictures", executeContext.Arguments.TrackId); + await publishEndpoint.Publish( + new TrackPictureProcessed(executeContext.Arguments.TrackId), + executeContext.CancellationToken); + return executeContext.Completed(); } catch (Exception exception) diff --git a/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index f31fcbe..d9eb52c 100644 --- a/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/backend/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Musify.Application.Contracts; using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.MassTransit.Sagas; using Musify.Infrastructure.Persistence; namespace Musify.Infrastructure.MassTransit @@ -66,6 +67,14 @@ namespace Musify.Infrastructure.MassTransit RegisterConsumersAndActivities(options); + options.AddSagaStateMachine() + .EntityFrameworkRepository(repository => + { + repository.ConcurrencyMode = ConcurrencyMode.Pessimistic; + repository.ExistingDbContext(); + repository.UsePostgres(); + }); + options.AddConfigureEndpointsCallback((registrationContext, _, endpointConfigurator) => { UseStandardRetry(endpointConfigurator); diff --git a/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingState.cs b/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingState.cs new file mode 100644 index 0000000..4d37377 --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingState.cs @@ -0,0 +1,19 @@ +using MassTransit; + +namespace Musify.Infrastructure.MassTransit.Sagas +{ + public class TrackProcessingState : SagaStateMachineInstance + { + public Guid CorrelationId { get; set; } + + public string CurrentState { get; set; } = null!; + + public bool PictureProcessed { get; set; } + + public bool AudioProcessed { 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 new file mode 100644 index 0000000..4b73233 --- /dev/null +++ b/backend/Musify.Infrastructure/MassTransit/Sagas/TrackProcessingStateMachine.cs @@ -0,0 +1,67 @@ +using MassTransit; +using Musify.Application.Events; + +namespace Musify.Infrastructure.MassTransit.Sagas +{ + public sealed class TrackProcessingStateMachine : MassTransitStateMachine + { + public State Processing { get; private set; } = null!; + public State Failed { get; private set; } = null!; + + public Event ProcessingStarted { get; private set; } = null!; + public Event PictureProcessed { get; private set; } = null!; + public Event AudioProcessed { get; private set; } = null!; + public Event PictureFailed { get; private set; } = null!; + public Event AudioFailed { get; private set; } = null!; + + public TrackProcessingStateMachine() + { + InstanceState(state => state.CurrentState); + + Event(() => ProcessingStarted, config => config.CorrelateById(context => context.Message.TrackId)); + Event(() => PictureProcessed, config => config.CorrelateById(context => context.Message.TrackId)); + Event(() => AudioProcessed, config => config.CorrelateById(context => context.Message.TrackId)); + Event(() => PictureFailed, config => config.CorrelateById(context => context.Message.TrackId)); + Event(() => AudioFailed, config => config.CorrelateById(context => context.Message.TrackId)); + + Initially( + When(ProcessingStarted) + .Then(context => + { + context.Saga.CreatedAt = DateTime.UtcNow; + context.Saga.UpdatedAt = DateTime.UtcNow; + }) + .TransitionTo(Processing)); + + During(Processing, + When(PictureProcessed) + .Then(context => + { + context.Saga.PictureProcessed = true; + context.Saga.UpdatedAt = DateTime.UtcNow; + }) + .If(context => context.Saga.AudioProcessed, binder => binder.Finalize()), + When(AudioProcessed) + .Then(context => + { + context.Saga.AudioProcessed = true; + context.Saga.UpdatedAt = DateTime.UtcNow; + }) + .If(context => context.Saga.PictureProcessed, binder => binder.Finalize()), + When(PictureFailed) + .Then(context => context.Saga.UpdatedAt = DateTime.UtcNow) + .TransitionTo(Failed), + When(AudioFailed) + .Then(context => context.Saga.UpdatedAt = DateTime.UtcNow) + .TransitionTo(Failed)); + + During(Failed, + Ignore(PictureProcessed), + Ignore(AudioProcessed), + Ignore(PictureFailed), + Ignore(AudioFailed)); + + SetCompletedWhenFinalized(); + } + } +} diff --git a/backend/Musify.Infrastructure/Persistence/Database.cs b/backend/Musify.Infrastructure/Persistence/Database.cs index 3723077..b4aa0e9 100644 --- a/backend/Musify.Infrastructure/Persistence/Database.cs +++ b/backend/Musify.Infrastructure/Persistence/Database.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore.Storage; using MassTransit; using Musify.Domain.Entities; using Musify.Infrastructure.Configuration; +using Musify.Infrastructure.MassTransit.Sagas; using AppIDatabase = Musify.Application.Contracts.IDatabase; using Musify.Application.Contracts; @@ -24,6 +25,11 @@ namespace Musify.Infrastructure.Persistence modelBuilder.AddInboxStateEntity(); modelBuilder.AddOutboxMessageEntity(); modelBuilder.AddOutboxStateEntity(); + + var trackProcessing = modelBuilder.Entity(); + trackProcessing.HasKey(state => state.CorrelationId); + trackProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); + trackProcessing.Property(state => state.CurrentState).HasMaxLength(64); } public DbSet Users => Set(); diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/20260705123932_AddTrackProcessingStateSaga.Designer.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260705123932_AddTrackProcessingStateSaga.Designer.cs new file mode 100644 index 0000000..a9244d0 --- /dev/null +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260705123932_AddTrackProcessingStateSaga.Designer.cs @@ -0,0 +1,610 @@ +// +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("20260705123932_AddTrackProcessingStateSaga")] + partial class AddTrackProcessingStateSaga + { + /// + 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("AudioProcessed") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + 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/20260705123932_AddTrackProcessingStateSaga.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260705123932_AddTrackProcessingStateSaga.cs new file mode 100644 index 0000000..2a0c159 --- /dev/null +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260705123932_AddTrackProcessingStateSaga.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddTrackProcessingStateSaga : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TrackProcessingState", + columns: table => new + { + CorrelationId = table.Column(type: "uuid", nullable: false), + CurrentState = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + PictureProcessed = table.Column(type: "boolean", nullable: false), + AudioProcessed = table.Column(type: "boolean", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TrackProcessingState", x => x.CorrelationId); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TrackProcessingState"); + } + } +} diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index b6f5653..da9d2fe 100644 --- a/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/backend/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -484,6 +484,33 @@ namespace Musify.Infrastructure.Persistence.Migrations b.ToTable("UserHasTrack"); }); + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("AudioProcessed") + .HasColumnType("boolean"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + 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)