From 588be7941c37e3e6e34c9fa45f979fe609da47e2 Mon Sep 17 00:00:00 2001 From: Dominic Villemure Date: Sat, 24 Aug 2024 18:39:09 -0400 Subject: [PATCH] Add and remove reaction from a content --- Ef.ps1 => EfApplicationDbContext.ps1 | 0 EfContentDbContext.ps1 | 1 + EfMessagingDbContext.ps1 | 1 + src/Web/Extensions/EnumExtensions.cs | 34 ++ src/Web/Features/Contents/Data/Content.cs | 2 +- .../Contents/Data/ContentDbContext.cs | 9 +- .../Features/Contents/Data/ContentReaction.cs | 11 + .../Features/Contents/Data/Enums/Reaction.cs | 13 + .../Features/Contents/Handlers/AddReaction.cs | 93 ++++++ .../Features/Contents/Handlers/GetContent.cs | 7 +- .../Contents/Handlers/GetContentsByCreator.cs | 103 +++--- .../Contents/Handlers/Models/ContentModel.cs | 5 +- .../Contents/Handlers/Models/ReactionModel.cs | 8 + .../Contents/Handlers/RemoveReaction.cs | 36 +++ ...824185551_AddReactionToContent.Designer.cs | 296 ++++++++++++++++++ .../20240824185551_AddReactionToContent.cs | 48 +++ .../ContentDbContextModelSnapshot.cs | 38 ++- 17 files changed, 660 insertions(+), 45 deletions(-) rename Ef.ps1 => EfApplicationDbContext.ps1 (100%) create mode 100644 EfContentDbContext.ps1 create mode 100644 EfMessagingDbContext.ps1 create mode 100644 src/Web/Extensions/EnumExtensions.cs create mode 100644 src/Web/Features/Contents/Data/ContentReaction.cs create mode 100644 src/Web/Features/Contents/Data/Enums/Reaction.cs create mode 100644 src/Web/Features/Contents/Handlers/AddReaction.cs create mode 100644 src/Web/Features/Contents/Handlers/Models/ReactionModel.cs create mode 100644 src/Web/Features/Contents/Handlers/RemoveReaction.cs create mode 100644 src/Web/Features/Contents/Migrations/20240824185551_AddReactionToContent.Designer.cs create mode 100644 src/Web/Features/Contents/Migrations/20240824185551_AddReactionToContent.cs diff --git a/Ef.ps1 b/EfApplicationDbContext.ps1 similarity index 100% rename from Ef.ps1 rename to EfApplicationDbContext.ps1 diff --git a/EfContentDbContext.ps1 b/EfContentDbContext.ps1 new file mode 100644 index 0000000..c484fd2 --- /dev/null +++ b/EfContentDbContext.ps1 @@ -0,0 +1 @@ +dotnet ef $args --startup-project ./src/Web/Web.csproj --project ./src/Web/Web.csproj --context ContentDbContext \ No newline at end of file diff --git a/EfMessagingDbContext.ps1 b/EfMessagingDbContext.ps1 new file mode 100644 index 0000000..6e5f39e --- /dev/null +++ b/EfMessagingDbContext.ps1 @@ -0,0 +1 @@ +dotnet ef $args --startup-project ./src/Web/Web.csproj --project ./src/Web/Web.csproj --context MessagingDbContext \ No newline at end of file diff --git a/src/Web/Extensions/EnumExtensions.cs b/src/Web/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..c820a5e --- /dev/null +++ b/src/Web/Extensions/EnumExtensions.cs @@ -0,0 +1,34 @@ +namespace Hutopy.Web.Extensions; + +public static class EnumExtensions +{ + /// + /// Converts a string to the specified enum type. + /// + /// The type of the enum to convert to. Must be an enum. + /// The string value to convert. + /// Specifies whether the string comparison should ignore case. Default is true. + /// + /// The corresponding enum value if the conversion is successful; otherwise, null if the string + /// cannot be converted to the specified enum type. + /// + public static TEnum? ToEnum(this string value, bool ignoreCase = true) where TEnum : struct + { + if (Enum.TryParse(value, ignoreCase, out TEnum result)) + { + return result; + } + return null; + } + + /// + /// Converts an enum value to its string representation. + /// + /// The type of the enum. + /// The enum value to convert. + /// The string representation of the enum value. + public static string FromEnum(this TEnum enumValue) where TEnum : struct, Enum + { + return enumValue.ToString(); + } +} diff --git a/src/Web/Features/Contents/Data/Content.cs b/src/Web/Features/Contents/Data/Content.cs index 3e5d03a..f782698 100644 --- a/src/Web/Features/Contents/Data/Content.cs +++ b/src/Web/Features/Contents/Data/Content.cs @@ -10,8 +10,8 @@ public class Content public DateTimeOffset CreatedAt { get; init; } public Guid? DeletedBy { get; set; } public DateTimeOffset? DeletedAt { get; set; } - [MaxLength(128)] public required string Title { get; set; } [MaxLength(2048)] public required string Description { get; set; } + public IList Reactions { get; set; } = new List(); public string[]? Urls { get; init; } } diff --git a/src/Web/Features/Contents/Data/ContentDbContext.cs b/src/Web/Features/Contents/Data/ContentDbContext.cs index c47a453..478c576 100644 --- a/src/Web/Features/Contents/Data/ContentDbContext.cs +++ b/src/Web/Features/Contents/Data/ContentDbContext.cs @@ -1,4 +1,6 @@ -namespace Hutopy.Web.Features.Contents.Data; +using Humanizer; + +namespace Hutopy.Web.Features.Contents.Data; public class ContentDbContext( DbContextOptions options) @@ -25,6 +27,11 @@ public class ContentDbContext( .HasOne(c => c.Creator) .WithMany() .HasForeignKey(c => c.CreatedBy); + + modelBuilder + .Entity() + .OwnsMany(c => c.Reactions) + .ToTable(nameof(ContentReaction).Pluralize()); modelBuilder .Entity() diff --git a/src/Web/Features/Contents/Data/ContentReaction.cs b/src/Web/Features/Contents/Data/ContentReaction.cs new file mode 100644 index 0000000..475bc86 --- /dev/null +++ b/src/Web/Features/Contents/Data/ContentReaction.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; +using Hutopy.Web.Features.Contents.Handlers.Enums; + +namespace Hutopy.Web.Features.Contents.Data; + +public class ContentReaction +{ + public required Reaction Reaction { get; set; } + public required Guid UserId { get; set; } + [MaxLength(128)] public required string UserName { get; set; } +} diff --git a/src/Web/Features/Contents/Data/Enums/Reaction.cs b/src/Web/Features/Contents/Data/Enums/Reaction.cs new file mode 100644 index 0000000..1e898db --- /dev/null +++ b/src/Web/Features/Contents/Data/Enums/Reaction.cs @@ -0,0 +1,13 @@ +namespace Hutopy.Web.Features.Contents.Handlers.Enums; + +public enum Reaction +{ + None = 0, + Like = 1, + Dislike = 2, + Love = 3, + Haha = 4, + Wow = 5, + Sad = 6, + Angry = 7 +} diff --git a/src/Web/Features/Contents/Handlers/AddReaction.cs b/src/Web/Features/Contents/Handlers/AddReaction.cs new file mode 100644 index 0000000..855ffa7 --- /dev/null +++ b/src/Web/Features/Contents/Handlers/AddReaction.cs @@ -0,0 +1,93 @@ +using Hutopy.Web.Extensions; +using Hutopy.Web.Features.Contents.Data; +using Hutopy.Web.Features.Contents.Handlers.Enums; + +namespace Hutopy.Web.Features.Contents.Handlers; + +[PublicAPI] +public sealed class AddReactionRequest +{ + public required Guid ContentId { get; set; } + public required string Reaction { get; set; } + public required Guid UserId { get; set; } + public required string UserName { get; set; } +} + +[PublicAPI] +internal sealed class AddReactionRequestValidator + : Validator +{ + public AddReactionRequestValidator() + { + RuleFor(r => r.Reaction) + .NotNull() + .Must(BeAValidReaction) + .WithMessage("'{PropertyValue}' is not a valid reaction."); + } + + private bool BeAValidReaction(string reaction) + { + return Enum.TryParse(typeof(Reaction), reaction, true, out _); + } +} + +[PublicAPI] +public class AddReaction( + ContentDbContext context) + : Endpoint +{ + public override void Configure() + { + Post("/api/content/reaction"); + Options(o => o.WithTags("Contents")); + } + + public override async Task HandleAsync( + AddReactionRequest req, + CancellationToken ct) + { + var content = await context.Contents.SingleAsync(x => x.Id == req.ContentId, ct); + var reactionEnum = req.Reaction.ToEnum(); + + var hasReacted = content.Reactions.Any(x => x.UserId == req.UserId); + + if (hasReacted) + { + var currentReaction = content.Reactions.Single(x => x.UserId == req.UserId); + if (currentReaction.Reaction == reactionEnum) return; + + if (reactionEnum.HasValue) + { + content.Reactions.Remove(currentReaction); + + var reaction = new ContentReaction + { + Reaction = reactionEnum.Value, + UserId = req.UserId, + UserName = req.UserName + }; + + content.Reactions.Add(reaction); + } + } + else + { + if (reactionEnum.HasValue) + { + var reaction = new ContentReaction + { + Reaction = reactionEnum.Value, + UserId = req.UserId, + UserName = req.UserName + }; + + content.Reactions.Add(reaction); + } + } + + await context.SaveChangesAsync(ct); + } +} + + + diff --git a/src/Web/Features/Contents/Handlers/GetContent.cs b/src/Web/Features/Contents/Handlers/GetContent.cs index 3fe1c38..fc89f32 100644 --- a/src/Web/Features/Contents/Handlers/GetContent.cs +++ b/src/Web/Features/Contents/Handlers/GetContent.cs @@ -1,4 +1,5 @@ -using Hutopy.Web.Features.Contents.Data; +using Hutopy.Web.Extensions; +using Hutopy.Web.Features.Contents.Data; using Hutopy.Web.Features.Contents.Handlers.Models; namespace Hutopy.Web.Features.Contents.Handlers; @@ -41,6 +42,10 @@ public class GetContent( Title = c.Title, Description = c.Description, Urls = c.Urls, + Reactions = c.Reactions.Select(x => new ReactionModel + { + Reaction = x.Reaction.FromEnum(), UserId = x.UserId, UserName = x.UserName + }).ToList() }) .SingleOrDefaultAsync( c => c.Id == req.ContentId, diff --git a/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs b/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs index 6392ee3..52247c9 100644 --- a/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs +++ b/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs @@ -1,4 +1,4 @@ -using System.Text; +using Hutopy.Web.Extensions; using Hutopy.Web.Features.Contents.Data; using Hutopy.Web.Features.Contents.Handlers.Models; @@ -28,45 +28,72 @@ public class GetContentsByCreatorHandler( GetContentsByCreatorRequest req, CancellationToken ct) { - var queryBuilder = new StringBuilder(); - queryBuilder.AppendLine($""" - SELECT content."Id", - content."CreatedBy", - creator."Name" as CreatedByName, - i."Logo" as CreatedByPortraitUrl, - c."Menu" as ColorMenu, - c."Accent" as ColorAccent, - content."CreatedAt", - content."DeletedBy", - content."DeletedAt", - content."Title", - content."Description", - content."Urls" - FROM "Content"."Contents" AS content - INNER JOIN "Content"."Creators" AS creator ON content."CreatedBy" = creator."Id" - LEFT JOIN "Content"."Images" AS i ON creator."Id" = i."CreatorId" - LEFT JOIN "Content"."Colors" AS c ON creator."Id" = c."CreatorId" - WHERE content."CreatedBy" = '{req.CreatorId}' - AND content."DeletedBy" IS NULL - """); +// var queryBuilder = new StringBuilder(); +// queryBuilder.AppendLine($""" +// SELECT content."Id", +// content."CreatedBy", +// creator."Name" as CreatedByName, +// i."Logo" as CreatedByPortraitUrl, +// c."Menu" as ColorMenu, +// c."Accent" as ColorAccent, +// content."CreatedAt", +// content."DeletedBy", +// content."DeletedAt", +// content."Title", +// content."Description", +// content."Urls", +// FROM "Content"."Contents" AS content +// INNER JOIN "Content"."Creators" AS creator ON content."CreatedBy" = creator."Id" +// LEFT JOIN "Content"."Images" AS i ON creator."Id" = i."CreatorId" +// LEFT JOIN "Content"."Colors" AS c ON creator."Id" = c."CreatorId" +// LEFT JOIN "Content"."ContentReactions" AS cr ON content."Id" = cr."ContentId" +// WHERE content."CreatedBy" = '{req.CreatorId}' +// AND content."DeletedBy" IS NULL +// """); +// +// if (req.LastId.HasValue) +// { +// queryBuilder.AppendLine($"""AND content."Id" > '{req.LastId.Value}'"""); +// } +// +// queryBuilder.AppendLine($""" +// ORDER BY content."CreatedAt" DESC +// LIMIT {req.PageSize} +// """); +// +// var query = queryBuilder.ToString(); +// +// var results = await context +// .Database +// .SqlQueryRaw(query) +// .Include(c => c.Reactions) +// .ToListAsync(cancellationToken: ct); - if (req.LastId.HasValue) - { - queryBuilder.AppendLine($"""AND content."Id" > '{req.LastId.Value}'"""); - } - queryBuilder.AppendLine($""" - ORDER BY content."CreatedAt" DESC - LIMIT {req.PageSize} - """); + var content = await context + .Contents + .Select(c => new ContentModel + { + Id = c.Id, + CreatedBy = c.CreatedBy, + CreatedByName = c.Creator!.Name, + CreatedByPortraitUrl = c.Creator.Images.Logo, + CreatedAt = c.CreatedAt, + ColorMenu = c.Creator.Colors.Menu, + ColorAccent = c.Creator.Colors.Accent, + DeletedBy = c.DeletedBy, + DeletedAt = c.DeletedAt, + Title = c.Title, + Description = c.Description, + Urls = c.Urls, + Reactions = c.Reactions.Select(x => new ReactionModel + { + Reaction = x.Reaction.FromEnum(), UserId = x.UserId, UserName = x.UserName + }).ToList() + }) + .Where(x => x.CreatedBy == req.CreatorId && x.DeletedAt == null) + .ToListAsync(ct); - var query = queryBuilder.ToString(); - - var results = await context - .Database - .SqlQueryRaw(query) - .ToListAsync(cancellationToken: ct); - - await SendAsync(results, cancellation: ct); + await SendAsync(content, cancellation: ct); } } diff --git a/src/Web/Features/Contents/Handlers/Models/ContentModel.cs b/src/Web/Features/Contents/Handlers/Models/ContentModel.cs index c3cecc9..c110b0b 100644 --- a/src/Web/Features/Contents/Handlers/Models/ContentModel.cs +++ b/src/Web/Features/Contents/Handlers/Models/ContentModel.cs @@ -1,4 +1,6 @@ -namespace Hutopy.Web.Features.Contents.Handlers.Models; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Hutopy.Web.Features.Contents.Handlers.Models; [PublicAPI] public class ContentModel @@ -15,4 +17,5 @@ public class ContentModel public required string Title { get; init; } public required string Description { get; init; } public required string[]? Urls { get; init; } + public IList? Reactions { get; set; } = new List(); } diff --git a/src/Web/Features/Contents/Handlers/Models/ReactionModel.cs b/src/Web/Features/Contents/Handlers/Models/ReactionModel.cs new file mode 100644 index 0000000..514efd3 --- /dev/null +++ b/src/Web/Features/Contents/Handlers/Models/ReactionModel.cs @@ -0,0 +1,8 @@ +namespace Hutopy.Web.Features.Contents.Handlers.Models; + +public class ReactionModel +{ + public required string Reaction { get; set; } + public required Guid UserId { get; set; } + public required string UserName { get; set; } +} diff --git a/src/Web/Features/Contents/Handlers/RemoveReaction.cs b/src/Web/Features/Contents/Handlers/RemoveReaction.cs new file mode 100644 index 0000000..04d2b43 --- /dev/null +++ b/src/Web/Features/Contents/Handlers/RemoveReaction.cs @@ -0,0 +1,36 @@ +using Hutopy.Web.Features.Contents.Data; + +namespace Hutopy.Web.Features.Contents.Handlers; + +[PublicAPI] +public sealed class RemoveReactionRequest +{ + public required Guid ContentId { get; set; } + public required Guid UserId { get; set; } +} + +[PublicAPI] +public class RemoveReaction( + ContentDbContext context) + : Endpoint +{ + public override void Configure() + { + Post("/api/content/reaction/remove"); + Options(o => o.WithTags("Contents")); + } + + public override async Task HandleAsync( + RemoveReactionRequest req, + CancellationToken ct) + { + var content = await context.Contents + .SingleAsync(x => x.Id == req.ContentId, ct); + + var reaction = content.Reactions.Single(x => x.UserId == req.UserId); + + content.Reactions.Remove(reaction); + + await context.SaveChangesAsync(ct); + } +} diff --git a/src/Web/Features/Contents/Migrations/20240824185551_AddReactionToContent.Designer.cs b/src/Web/Features/Contents/Migrations/20240824185551_AddReactionToContent.Designer.cs new file mode 100644 index 0000000..8f13557 --- /dev/null +++ b/src/Web/Features/Contents/Migrations/20240824185551_AddReactionToContent.Designer.cs @@ -0,0 +1,296 @@ +// +using System; +using Hutopy.Web.Features.Contents.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Hutopy.Web.Features.Contents.Migrations +{ + [DbContext(typeof(ContentDbContext))] + [Migration("20240824185551_AddReactionToContent")] + partial class AddReactionToContent + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Content") + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Urls") + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Contents", "Content"); + }); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("Creators", "Content"); + }); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Subscription", b => + { + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CreatedBy", "CreatorId"); + + b.HasIndex("CreatorId"); + + b.ToTable("Subscriptions", "Content"); + }); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Content", b => + { + b.HasOne("Hutopy.Web.Features.Contents.Data.Creator", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Hutopy.Web.Features.Contents.Data.ContentReaction", "Reactions", b1 => + { + b1.Property("ContentId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Reaction") + .HasColumnType("integer"); + + b1.Property("UserId") + .HasColumnType("uuid"); + + b1.Property("UserName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.HasKey("ContentId", "Id"); + + b1.ToTable("ContentReactions", "Content"); + + b1.WithOwner() + .HasForeignKey("ContentId"); + }); + + b.Navigation("Creator"); + + b.Navigation("Reactions"); + }); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b => + { + b.OwnsOne("Hutopy.Web.Features.Contents.Data.About", "About", b1 => + { + b1.Property("CreatorId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b1.Property("Title") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.HasKey("CreatorId"); + + b1.ToTable("Creators", "Content"); + + b1.WithOwner() + .HasForeignKey("CreatorId"); + }); + + b.OwnsOne("Hutopy.Web.Features.Contents.Data.Colors", "Colors", b1 => + { + b1.Property("CreatorId") + .HasColumnType("uuid"); + + b1.Property("Accent") + .HasMaxLength(9) + .HasColumnType("character varying(9)"); + + b1.Property("BannerBottom") + .HasMaxLength(9) + .HasColumnType("character varying(9)"); + + b1.Property("BannerTop") + .HasMaxLength(9) + .HasColumnType("character varying(9)"); + + b1.Property("Menu") + .HasMaxLength(9) + .HasColumnType("character varying(9)"); + + b1.HasKey("CreatorId"); + + b1.ToTable("Colors", "Content"); + + b1.WithOwner() + .HasForeignKey("CreatorId"); + }); + + b.OwnsOne("Hutopy.Web.Features.Contents.Data.Images", "Images", b1 => + { + b1.Property("CreatorId") + .HasColumnType("uuid"); + + b1.Property("Banner") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.Property("Logo") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.HasKey("CreatorId"); + + b1.ToTable("Images", "Content"); + + b1.WithOwner() + .HasForeignKey("CreatorId"); + }); + + b.OwnsOne("Hutopy.Web.Features.Contents.Data.Socials", "Socials", b1 => + { + b1.Property("CreatorId") + .HasColumnType("uuid"); + + b1.Property("FacebookUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.Property("InstagramUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.Property("LinkedInUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.Property("RedditUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.Property("TikTokUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.Property("WebsiteUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.Property("XUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.Property("YoutubeUrl") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b1.HasKey("CreatorId"); + + b1.ToTable("Socials", "Content"); + + b1.WithOwner() + .HasForeignKey("CreatorId"); + }); + + b.Navigation("About") + .IsRequired(); + + b.Navigation("Colors") + .IsRequired(); + + b.Navigation("Images") + .IsRequired(); + + b.Navigation("Socials") + .IsRequired(); + }); + + modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Subscription", b => + { + b.HasOne("Hutopy.Web.Features.Contents.Data.Creator", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Web/Features/Contents/Migrations/20240824185551_AddReactionToContent.cs b/src/Web/Features/Contents/Migrations/20240824185551_AddReactionToContent.cs new file mode 100644 index 0000000..6b28a31 --- /dev/null +++ b/src/Web/Features/Contents/Migrations/20240824185551_AddReactionToContent.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Hutopy.Web.Features.Contents.Migrations +{ + /// + public partial class AddReactionToContent : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ContentReactions", + schema: "Content", + columns: table => new + { + ContentId = table.Column(type: "uuid", nullable: false), + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Reaction = table.Column(type: "integer", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + UserName = table.Column(type: "character varying(128)", maxLength: 128, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ContentReactions", x => new { x.ContentId, x.Id }); + table.ForeignKey( + name: "FK_ContentReactions_Contents_ContentId", + column: x => x.ContentId, + principalSchema: "Content", + principalTable: "Contents", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ContentReactions", + schema: "Content"); + } + } +} diff --git a/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs b/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs index e4821b9..a3d9e3f 100644 --- a/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs +++ b/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs @@ -37,12 +37,12 @@ namespace Hutopy.Web.Features.Contents.Migrations b.Property("CreatedBy") .HasColumnType("uuid"); - b.Property("DeletedBy") - .HasColumnType("uuid"); - b.Property("DeletedAt") .HasColumnType("timestamp with time zone"); + b.Property("DeletedBy") + .HasColumnType("uuid"); + b.Property("Description") .IsRequired() .HasMaxLength(2048) @@ -111,7 +111,39 @@ namespace Hutopy.Web.Features.Contents.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.OwnsMany("Hutopy.Web.Features.Contents.Data.ContentReaction", "Reactions", b1 => + { + b1.Property("ContentId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Reaction") + .HasColumnType("integer"); + + b1.Property("UserId") + .HasColumnType("uuid"); + + b1.Property("UserName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b1.HasKey("ContentId", "Id"); + + b1.ToTable("ContentReactions", "Content"); + + b1.WithOwner() + .HasForeignKey("ContentId"); + }); + b.Navigation("Creator"); + + b.Navigation("Reactions"); }); modelBuilder.Entity("Hutopy.Web.Features.Contents.Data.Creator", b =>