Clean up domain entities: shared picture value object, lifecycle and ownership interfaces, explicit tables

This commit is contained in:
2026-09-24 11:43:50 +02:00
parent 4d943e068e
commit 5a0f82b77b
45 changed files with 122 additions and 190 deletions
@@ -56,7 +56,7 @@ public class CreateAlbumCommandHandler(
NormalizedTitle = title.ToUpperInvariant(),
Description = request.Description,
ReleaseYear = request.ReleaseYear,
Pictures = AlbumPictures.Pending(intent.ObjectName)
Pictures = EntityPictures.Pending(intent.ObjectName)
};
await database.Albums.AddAsync(album, cancellationToken);
@@ -64,7 +64,7 @@ public class UpdateAlbumCommandHandler(
return validation.Errors;
var pictureIntent = validation.Value;
album.Pictures = AlbumPictures.Pending(pictureIntent.ObjectName);
album.Pictures = EntityPictures.Pending(pictureIntent.ObjectName);
var finalPictureKey = albumConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
var publishResult = await PublishUpdateAlbumPictureSourceEventAsync(
@@ -24,7 +24,7 @@ public class AddTrackToPlayListCommandHandler(
if (playList == null)
return AppErrors.NotFound("PlayList", request.PlayListId);
if (playList.UserId != request.UserId)
if (playList.OwnerUserId != request.UserId)
return AppErrors.Forbidden("PlayList", request.PlayListId);
var trackExists = await database.Tracks
@@ -17,7 +17,7 @@ public record CreatePlayListCommand(
string Name,
string? Description,
Guid? PictureIntentId,
PlaylistVisibility Visibility = PlaylistVisibility.Private)
PlayListVisibility Visibility = PlayListVisibility.Private)
: ICommand<ErrorOr<PlayListApplicationResponse>>;
public class CreatePlayListCommandHandler(
@@ -30,7 +30,7 @@ public class CreatePlayListCommandHandler(
UploadIntentConfiguration uploadIntentConfiguration)
: ICommandHandler<CreatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
{
private sealed record ResolvedPlayListPictures(PlayListPictures? Pictures, UploadIntent? Intent);
private sealed record ResolvedPlayListPictures(EntityPictures? Pictures, UploadIntent? Intent);
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(CreatePlayListCommand request, CancellationToken cancellationToken)
{
@@ -51,7 +51,7 @@ public class CreatePlayListCommandHandler(
var playList = new PlayList
{
UserId = request.UserId,
OwnerUserId = request.UserId,
Name = request.Name.Trim(),
NormalizedName = request.Name.Trim().ToUpperInvariant(),
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
@@ -96,7 +96,7 @@ public class CreatePlayListCommandHandler(
if (validation.IsError)
return validation.Errors;
return new ResolvedPlayListPictures(PlayListPictures.Pending(validation.Value.ObjectName), validation.Value);
return new ResolvedPlayListPictures(EntityPictures.Pending(validation.Value.ObjectName), validation.Value);
}
private async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
@@ -27,7 +27,7 @@ public class DeletePlayListCommandHandler(
return Error.NotFound();
}
if (playList.UserId != request.UserId)
if (playList.OwnerUserId != request.UserId)
{
logger.LogWarning("Playlist {PlayListId} does not belong to user {UserId}", request.PlayListId, request.UserId);
return AppErrors.Forbidden("PlayList", request.PlayListId);
@@ -32,8 +32,8 @@ public class GetPlayListByIdQueryHandler(IDatabase database)
if (entry == null)
return Error.NotFound();
if (entry.PlayList.Visibility == PlaylistVisibility.Private
&& entry.PlayList.UserId != request.RequestingUserId)
if (entry.PlayList.Visibility == PlayListVisibility.Private
&& entry.PlayList.OwnerUserId != request.RequestingUserId)
{
return Error.NotFound();
}
@@ -24,7 +24,7 @@ public class GetPlayListCoverQueryHandler(
{
var owned = await database.PlayLists.AsNoTracking()
.Where(e => e.Id == request.PlayListId && e.LifeCycleStatus == LifeCycleStatus.Active)
.Select(e => new { OwnerUserId = e.UserId, e.Pictures })
.Select(e => new { OwnerUserId = e.OwnerUserId, e.Pictures })
.SingleOrDefaultAsync(cancellationToken);
if (owned?.Pictures == null)
@@ -27,7 +27,7 @@ public class GetPlayListTracksQueryHandler(IDatabase database)
.AnyAsync(
p => p.Id == request.PlayListId
&& p.LifeCycleStatus == LifeCycleStatus.Active
&& (p.Visibility == PlaylistVisibility.Public || p.UserId == request.ViewerId),
&& (p.Visibility == PlayListVisibility.Public || p.OwnerUserId == request.ViewerId),
cancellationToken);
if (!playListExists)
return Error.NotFound();
@@ -25,9 +25,9 @@ public class GetPlayListsByUserIdQueryHandler(IDatabase database)
{
var playListsQuery = database.PlayLists
.AsNoTracking()
.Where(p => p.UserId == request.UserId
.Where(p => p.OwnerUserId == request.UserId
&& p.LifeCycleStatus == LifeCycleStatus.Active
&& (p.Visibility == PlaylistVisibility.Public || p.UserId == request.ViewerId));
&& (p.Visibility == PlayListVisibility.Public || p.OwnerUserId == request.ViewerId));
if (!string.IsNullOrEmpty(request.Name))
{
@@ -23,7 +23,7 @@ public class RemoveTrackFromPlayListCommandHandler(
if (playList == null)
return AppErrors.NotFound("PlayList", request.PlayListId);
if (playList.UserId != request.UserId)
if (playList.OwnerUserId != request.UserId)
return AppErrors.Forbidden("PlayList", request.PlayListId);
var playListTracks = await database.PlayListHasTracks
@@ -10,7 +10,7 @@ public record PlayListApplicationResponse(
string? SmallImageKeyName,
string? MediumImageKeyName,
string? LargeImageKeyName,
PlaylistVisibility Visibility,
PlayListVisibility Visibility,
DateTime CreatedAt,
DateTime UpdatedAt,
IReadOnlyList<Guid> CoverTrackIds)
@@ -19,7 +19,7 @@ public record UpdatePlayListCommand(
string? NewName,
string? NewDescription,
Guid? NewPictureIntentId,
PlaylistVisibility? NewVisibility = null)
PlayListVisibility? NewVisibility = null)
: ICommand<ErrorOr<PlayListApplicationResponse>>;
public class UpdatePlayListCommandHandler(
@@ -41,7 +41,7 @@ public class UpdatePlayListCommandHandler(
return Error.NotFound();
}
if (playListEntity.UserId != request.UserId)
if (playListEntity.OwnerUserId != request.UserId)
{
logger.LogWarning("User {UserId} is not the owner of playlist {PlayListId}", request.UserId, request.PlayListId);
return AppErrors.Forbidden("PlayList", request.PlayListId);
@@ -72,7 +72,7 @@ public class UpdatePlayListCommandHandler(
return validation.Errors;
var pictureIntent = validation.Value;
playListEntity.Pictures = PlayListPictures.Pending(pictureIntent.ObjectName);
playListEntity.Pictures = EntityPictures.Pending(pictureIntent.ObjectName);
var finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
var publishResult = await PublishUpdatePlayListPictureSourceEventAsync(
@@ -52,7 +52,7 @@ public sealed class UploadIntentValidator(
if (intent.IsConsumed)
return Error.Conflict(description: "Upload intent has already been consumed.");
if (intent.IsExpired)
if (intent.IsExpiredAt(DateTime.UtcNow))
return Error.Validation(description: "Upload intent has expired.");
var metadata = await storageService.HeadObjectAsync(intent.Bucket, intent.Key, cancellationToken);
@@ -0,0 +1,10 @@
using Musify.Domain.ValueObjects;
namespace Musify.Domain.Abstractions;
public interface IHasLifeCycle
{
public Guid Id { get; }
public LifeCycleStatus LifeCycleStatus { get; set; }
}
@@ -0,0 +1,8 @@
namespace Musify.Domain.Abstractions;
public interface IOwnedEntity
{
public Guid Id { get; }
public long OwnerUserId { get; }
}
+4 -10
View File
@@ -5,17 +5,15 @@ using Musify.Domain.ValueObjects;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
public class Album : IAuditable
[Table("Albums")]
public class Album : IAuditable, IHasLifeCycle, IOwnedEntity
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(200)]
public required string Title { get; set; }
[Required]
[MaxLength(200)]
public required string NormalizedTitle { get; set; }
@@ -24,21 +22,17 @@ public class Album : IAuditable
public int? ReleaseYear { get; set; }
public AlbumPictures? Pictures { get; set; }
public EntityPictures? Pictures { get; set; }
[Required]
public required long OwnerUserId { get; set; }
[ForeignKey(nameof(OwnerUserId))]
public User Owner { get; set; }
public User Owner { get; set; } = null!;
[Required]
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Required]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<AlbumHasTrack> AlbumTracks { get; set; } = new List<AlbumHasTrack>();
@@ -1,29 +1,23 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
[Table("AlbumHasTrack")]
public class AlbumHasTrack
{
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required Guid AlbumId { get; set; }
[Required]
public required Guid TrackId { get; set; }
[Required]
public int TrackNumber { get; set; } = 1;
[Required]
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(AlbumId))]
public Album Album { get; set; }
public Album Album { get; set; } = null!;
[ForeignKey(nameof(TrackId))]
public Track Track { get; set; }
public Track Track { get; set; } = null!;
}
@@ -3,20 +3,16 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
[Table("ListeningHistory")]
public class ListeningHistory
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required long UserId { get; set; }
[Required]
public required Guid TrackId { get; set; }
[Required]
public DateTime ListenedAt { get; set; } = DateTime.UtcNow;
public double? PlayedSeconds { get; set; }
@@ -26,8 +22,8 @@ public class ListeningHistory
public bool IsCounted { get; set; }
[ForeignKey(nameof(UserId))]
public User User { get; set; }
public User User { get; set; } = null!;
[ForeignKey(nameof(TrackId))]
public Track Track { get; set; }
public Track Track { get; set; } = null!;
}
+2 -8
View File
@@ -5,34 +5,28 @@ using Musify.Domain.Abstractions;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
[Table("Mixes")]
public class Mix : IAuditable
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required long UserId { get; set; }
[Required]
[MaxLength(100)]
public required string Title { get; set; }
[MaxLength(256)]
public string? Subtitle { get; set; }
[Required]
public int Position { get; set; } = 0;
public int Position { get; set; }
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Required]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(UserId))]
public User User { get; set; }
public User User { get; set; } = null!;
public ICollection<MixItem> Items { get; set; } = new List<MixItem>();
}
@@ -3,25 +3,21 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
[Table("MixItems")]
public class MixItem
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required Guid MixId { get; set; }
[Required]
public int Position { get; set; } = 0;
public int Position { get; set; }
[Required]
public required Guid TrackId { get; set; }
[ForeignKey(nameof(MixId))]
public Mix Mix { get; set; }
public Mix Mix { get; set; } = null!;
[ForeignKey(nameof(TrackId))]
public Track Track { get; set; }
public Track Track { get; set; } = null!;
}
@@ -6,43 +6,36 @@ using Musify.Domain.ValueObjects;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
[Table("PlayLists")]
public class PlayList : IAuditable
public class PlayList : IAuditable, IHasLifeCycle, IOwnedEntity
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required long UserId { get; set; }
[Column("UserId")]
public required long OwnerUserId { get; set; }
[Required]
[MaxLength(50)]
public required string Name { get; set; }
[Required]
[MaxLength(50)]
public required string NormalizedName { get; set; }
[MaxLength(256)]
public string? Description { get; set; }
public PlayListPictures? Pictures { get; set; }
public EntityPictures? Pictures { get; set; }
[Required]
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
[Required]
public PlaylistVisibility Visibility { get; set; } = PlaylistVisibility.Private;
public PlayListVisibility Visibility { get; set; } = PlayListVisibility.Private;
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Required]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(UserId))]
public User User { get; set; }
[ForeignKey(nameof(OwnerUserId))]
public User Owner { get; set; } = null!;
public ICollection<PlayListHasTrack> PlayListTracks { get; set; } = new List<PlayListHasTrack>();
}
@@ -1,29 +1,23 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
[Table("PlayListHasTrack")]
public class PlayListHasTrack
{
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required Guid PlayListId { get; set; }
[Required]
public required Guid TrackId { get; set; }
[Required]
public int Position { get; set; } = 0;
public int Position { get; set; }
[Required]
public DateTime AddedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(PlayListId))]
public PlayList PlayList { get; set; }
public PlayList PlayList { get; set; } = null!;
[ForeignKey(nameof(TrackId))]
public Track Track { get; set; }
public Track Track { get; set; } = null!;
}
+3 -11
View File
@@ -5,39 +5,31 @@ using Musify.Domain.ValueObjects;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
public class Track : IAuditable
[Table("Tracks")]
public class Track : IAuditable, IHasLifeCycle, IOwnedEntity
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(200)]
public required string Title { get; set; }
[Required]
[MaxLength(200)]
public required string NormalizedTitle { get; set; }
[Required]
public double DurationSeconds { get; set; }
[Required]
public required long OwnerUserId { get; set; }
[ForeignKey(nameof(OwnerUserId))]
public required User Owner { get; set; }
public User Owner { get; set; } = null!;
[Required]
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
[Required]
public bool IsExplicit { get; set; }
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Required]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public TrackPictures Pictures { get; set; } = new();
@@ -3,25 +3,21 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
[Table("TrackLikes")]
public class TrackLike
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required long UserId { get; set; }
[Required]
public required Guid TrackId { get; set; }
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(UserId))]
public User User { get; set; }
public User User { get; set; } = null!;
[ForeignKey(nameof(TrackId))]
public Track Track { get; set; }
public Track Track { get; set; } = null!;
}
@@ -4,18 +4,16 @@ using Musify.Domain.ValueObjects;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
[Table("TrackTags")]
public class TrackTag
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required Guid TrackId { get; set; }
[Required]
public required Genre Tag { get; set; }
[ForeignKey(nameof(TrackId))]
public Track Track { get; set; }
public Track Track { get; set; } = null!;
}
@@ -11,40 +11,34 @@ public class UploadIntent
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required long UserId { get; set; }
[Required, MaxLength(128)]
[MaxLength(128)]
public required string Bucket { get; set; }
[Required, MaxLength(512)]
[MaxLength(512)]
public required string Key { get; set; }
[Required, MaxLength(128)]
[MaxLength(128)]
public required string ObjectName { get; set; }
[Required, MaxLength(128)]
[MaxLength(128)]
public required string ContentType { get; set; }
public long? ExpectedSizeBytes { get; set; }
[Required]
public UploadIntentPurpose Purpose { get; set; }
[Required]
public UploadIntentStatus Status { get; set; } = UploadIntentStatus.Issued;
[Required]
public DateTime ExpiresAt { get; set; }
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[NotMapped]
public bool IsConsumed => Status == UploadIntentStatus.Consumed;
[NotMapped]
public bool IsExpired => Status == UploadIntentStatus.Expired || ExpiresAt < DateTime.UtcNow;
public bool IsExpiredAt(DateTime now) => Status == UploadIntentStatus.Expired || ExpiresAt < now;
[ForeignKey(nameof(UserId))]
public User User { get; set; } = null!;
@@ -11,11 +11,9 @@ public class User : IAuditable
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public required long Id { get; set; }
[Required]
[MaxLength(48)]
public required string Name { get; set; }
[Required]
[MaxLength(48)]
public required string NormalizedName { get; set; }
@@ -28,10 +26,8 @@ public class User : IAuditable
[MaxLength(2048)]
public string? ProfilePictureUrl { get; set; }
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[Required]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<UserHasTrack> UserTracks { get; set; } = new List<UserHasTrack>();
@@ -3,25 +3,21 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
[Table("UserFollows")]
public class UserFollow
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required long FollowerId { get; set; }
[Required]
public required long FollowedId { get; set; }
[Required]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(FollowerId))]
public User Follower { get; set; }
public User Follower { get; set; } = null!;
[ForeignKey(nameof(FollowedId))]
public User Followed { get; set; }
public User Followed { get; set; } = null!;
}
@@ -1,23 +1,19 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Musify.Domain.Entities;
#pragma warning disable CS8618
[Table("UserHasTrack")]
public class UserHasTrack
{
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required long UserId { get; set; }
[Required]
public required Guid TrackId { get; set; }
[ForeignKey(nameof(UserId))]
public User User { get; set; }
public User User { get; set; } = null!;
[ForeignKey(nameof(TrackId))]
public Track Track { get; set; }
public Track Track { get; set; } = null!;
}
@@ -2,7 +2,8 @@ using System.ComponentModel.DataAnnotations;
namespace Musify.Domain.ValueObjects;
public class AlbumPictures
/// <summary>The picture file names of an album or playlist. Resized names stay null until processing finishes.</summary>
public class EntityPictures
{
[Required]
public required string OriginalName { get; set; }
@@ -13,5 +14,5 @@ public class AlbumPictures
public string? LargeName { get; set; }
public static AlbumPictures Pending(string originalName) => new() { OriginalName = originalName };
public static EntityPictures Pending(string objectName) => new() { OriginalName = objectName };
}
@@ -1,17 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Musify.Domain.ValueObjects;
public class PlayListPictures
{
[Required]
public required string OriginalName { get; set; }
public string? SmallName { get; set; }
public string? MediumName { get; set; }
public string? LargeName { get; set; }
public static PlayListPictures Pending(string originalName) => new() { OriginalName = originalName };
}
@@ -1,6 +1,6 @@
namespace Musify.Domain.ValueObjects;
public enum PlaylistVisibility
public enum PlayListVisibility
{
Private = 0,
Public = 1
@@ -89,7 +89,7 @@ internal class UpdateAlbumPictureActivity(
return compensateContext.Compensated();
}
album.Pictures = new AlbumPictures
album.Pictures = new EntityPictures
{
OriginalName = compensateContext.Log.PreviousOriginalPictureKey,
SmallName = compensateContext.Log.PreviousSmallPictureKey,
@@ -89,7 +89,7 @@ internal class UpdatePlayListPictureActivity(
return compensateContext.Compensated();
}
playList.Pictures = new PlayListPictures
playList.Pictures = new EntityPictures
{
OriginalName = compensateContext.Log.PreviousOriginalPictureKey,
SmallName = compensateContext.Log.PreviousSmallPictureKey,
@@ -64,7 +64,7 @@ public class Database(DatabaseConfiguration configuration)
modelBuilder.Entity<PlayList>()
.Property(playList => playList.Visibility)
.HasDefaultValue(PlaylistVisibility.Private);
.HasDefaultValue(PlayListVisibility.Private);
modelBuilder.Entity<PlayList>()
.OwnsOne(playList => playList.Pictures, pictures =>
@@ -379,12 +379,13 @@ namespace Musify.Infrastructure.Persistence.Migrations
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<long>("OwnerUserId")
.HasColumnType("bigint")
.HasColumnName("UserId");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<long>("UserId")
.HasColumnType("bigint");
b.Property<int>("Visibility")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
@@ -392,7 +393,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("OwnerUserId");
b.ToTable("PlayLists");
});
@@ -756,7 +757,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.OwnsOne("Musify.Domain.ValueObjects.AlbumPictures", "Pictures", b1 =>
b.OwnsOne("Musify.Domain.ValueObjects.EntityPictures", "Pictures", b1 =>
{
b1.Property<Guid>("AlbumId")
.HasColumnType("uuid");
@@ -865,13 +866,13 @@ namespace Musify.Infrastructure.Persistence.Migrations
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
{
b.HasOne("Musify.Domain.Entities.User", "User")
b.HasOne("Musify.Domain.Entities.User", "Owner")
.WithMany("PlayLists")
.HasForeignKey("UserId")
.HasForeignKey("OwnerUserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("Musify.Domain.ValueObjects.PlayListPictures", "Pictures", b1 =>
b.OwnsOne("Musify.Domain.ValueObjects.EntityPictures", "Pictures", b1 =>
{
b1.Property<Guid>("PlayListId")
.HasColumnType("uuid");
@@ -904,9 +905,9 @@ namespace Musify.Infrastructure.Persistence.Migrations
.HasForeignKey("PlayListId");
});
b.Navigation("Pictures");
b.Navigation("Owner");
b.Navigation("User");
b.Navigation("Pictures");
});
modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b =>
@@ -6,4 +6,4 @@ public record CreatePlayListRequest(
string Name,
string? Description,
Guid? PictureIntentId,
PlaylistVisibility Visibility = PlaylistVisibility.Private);
PlayListVisibility Visibility = PlayListVisibility.Private);
@@ -6,4 +6,4 @@ public record UpdatePlayListRequest(
string? NewName,
string? NewDescription,
Guid? NewPictureIntentId,
PlaylistVisibility? NewVisibility = null);
PlayListVisibility? NewVisibility = null);
@@ -39,7 +39,7 @@ public sealed class GetAlbumCoverQueryHandlerTests : HandlerTestBase
public async Task Handle_PicturesStillProcessing_FallsBackToTheOriginal()
{
var owner = TestEntities.User();
var album = TestEntities.Album(owner.Id, pictures: Musify.Domain.ValueObjects.AlbumPictures.Pending("original.png"));
var album = TestEntities.Album(owner.Id, pictures: Musify.Domain.ValueObjects.EntityPictures.Pending("original.png"));
await SeedAsync(owner, album);
var result = await CreateHandler().Handle(new GetAlbumCoverQuery(album.Id, "small"), TestContext.Current.CancellationToken);
@@ -71,7 +71,7 @@ public sealed class CreatePlayListCommandHandlerTests : HandlerTestBase
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Private, result.Value.Visibility);
Assert.Equal(Musify.Domain.ValueObjects.PlayListVisibility.Private, result.Value.Visibility);
}
[Fact]
@@ -80,12 +80,12 @@ public sealed class CreatePlayListCommandHandlerTests : HandlerTestBase
var user = TestEntities.User();
await SeedAsync(user);
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null, Visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null, Visibility: Musify.Domain.ValueObjects.PlayListVisibility.Public);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
Assert.Equal(Musify.Domain.ValueObjects.PlayListVisibility.Public, result.Value.Visibility);
}
[Fact]
@@ -14,7 +14,7 @@ public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
public async Task Handle_ExistingPlayList_ReturnsItWithCoverTrackIds()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlayListVisibility.Public);
var track = TestEntities.Track(owner);
await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 });
@@ -37,7 +37,7 @@ public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
public async Task Handle_PrivatePlayList_RequestedByOwner_ReturnsIt()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlayListVisibility.Private);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, owner.Id), TestContext.Current.CancellationToken);
@@ -50,7 +50,7 @@ public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
{
var owner = TestEntities.User(1, "owner");
var stranger = TestEntities.User(2, "stranger");
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlayListVisibility.Private);
await SeedAsync(owner, stranger, playList);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, stranger.Id), TestContext.Current.CancellationToken);
@@ -62,7 +62,7 @@ public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
public async Task Handle_PrivatePlayList_RequestedAnonymously_ReturnsNotFound()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private);
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlayListVisibility.Private);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken);
@@ -74,7 +74,7 @@ public sealed class GetPlayListByIdQueryHandlerTests : HandlerTestBase
public async Task Handle_PublicPlayList_RequestedAnonymously_ReturnsIt()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlayListVisibility.Public);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken);
@@ -41,7 +41,7 @@ public sealed class GetPlayListTracksQueryHandlerTests : HandlerTestBase
public async Task Handle_PrivatePlayListOfAnotherUser_ReturnsNotFound()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: PlaylistVisibility.Private);
var playList = TestEntities.PlayList(owner.Id, visibility: PlayListVisibility.Private);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(playList.Id, PageNumber: 1, PageSize: 10, ViewerId: 999), TestContext.Current.CancellationToken);
@@ -53,7 +53,7 @@ public sealed class GetPlayListTracksQueryHandlerTests : HandlerTestBase
public async Task Handle_PrivatePlayListOfViewer_ReturnsTracks()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: PlaylistVisibility.Private);
var playList = TestEntities.PlayList(owner.Id, visibility: PlayListVisibility.Private);
await SeedAsync(owner, playList);
var result = await CreateHandler().Handle(new GetPlayListTracksQuery(playList.Id, PageNumber: 1, PageSize: 10, ViewerId: owner.Id), TestContext.Current.CancellationToken);
@@ -33,8 +33,8 @@ public sealed class GetPlayListsByUserIdQueryHandlerTests : HandlerTestBase
var owner = TestEntities.User();
await SeedAsync(
owner,
TestEntities.PlayList(owner.Id, "Public one", visibility: PlaylistVisibility.Public),
TestEntities.PlayList(owner.Id, "Private one", visibility: PlaylistVisibility.Private));
TestEntities.PlayList(owner.Id, "Public one", visibility: PlayListVisibility.Public),
TestEntities.PlayList(owner.Id, "Private one", visibility: PlayListVisibility.Private));
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10, ViewerId: 999), TestContext.Current.CancellationToken);
@@ -49,8 +49,8 @@ public sealed class GetPlayListsByUserIdQueryHandlerTests : HandlerTestBase
var owner = TestEntities.User();
await SeedAsync(
owner,
TestEntities.PlayList(owner.Id, "Public one", visibility: PlaylistVisibility.Public),
TestEntities.PlayList(owner.Id, "Private one", visibility: PlaylistVisibility.Private));
TestEntities.PlayList(owner.Id, "Public one", visibility: PlayListVisibility.Public),
TestEntities.PlayList(owner.Id, "Private one", visibility: PlayListVisibility.Private));
var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10, ViewerId: owner.Id), TestContext.Current.CancellationToken);
@@ -69,19 +69,19 @@ public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
var playList = TestEntities.PlayList(owner.Id);
await SeedAsync(owner, playList);
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, null, Musify.Domain.ValueObjects.PlaylistVisibility.Public);
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, null, Musify.Domain.ValueObjects.PlayListVisibility.Public);
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
Assert.Equal(Musify.Domain.ValueObjects.PlayListVisibility.Public, result.Value.Visibility);
}
[Fact]
public async Task Handle_NoVisibilitySpecified_LeavesItUnchanged()
{
var owner = TestEntities.User();
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public);
var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlayListVisibility.Public);
await SeedAsync(owner, playList);
var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null);
@@ -89,7 +89,7 @@ public sealed class UpdatePlayListCommandHandlerTests : HandlerTestBase
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
Assert.False(result.IsError);
Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility);
Assert.Equal(Musify.Domain.ValueObjects.PlayListVisibility.Public, result.Value.Visibility);
}
[Fact]
@@ -79,7 +79,7 @@ public static class TestEntities
return track;
}
public static PlayListPictures PlayListPictures() => new()
public static EntityPictures PlayListPictures() => new()
{
OriginalName = "original.webp",
SmallName = "small.webp",
@@ -91,12 +91,12 @@ public static class TestEntities
long userId,
string name = "Test Playlist",
string? description = null,
PlayListPictures? pictures = null,
EntityPictures? pictures = null,
LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active,
PlaylistVisibility visibility = PlaylistVisibility.Private) =>
PlayListVisibility visibility = PlayListVisibility.Private) =>
new()
{
UserId = userId,
OwnerUserId = userId,
Name = name,
NormalizedName = name.ToUpperInvariant(),
Description = description,
@@ -105,7 +105,7 @@ public static class TestEntities
Visibility = visibility
};
public static AlbumPictures AlbumPictures() => new()
public static EntityPictures AlbumPictures() => new()
{
OriginalName = "original.webp",
SmallName = "small.webp",
@@ -118,7 +118,7 @@ public static class TestEntities
string title = "Test Album",
string? description = null,
int? releaseYear = 2024,
AlbumPictures? pictures = null) =>
EntityPictures? pictures = null) =>
new()
{
Title = title,