From 8b702d16d6ae8b5691dea6c3b29df7e113e3c43e Mon Sep 17 00:00:00 2001 From: Jonathan Bourdon Date: Fri, 16 Aug 2024 18:14:48 -0400 Subject: [PATCH] Adds Content deletion --- src/Web/Features/Contents/Data/Content.cs | 2 + .../Contents/Handlers/DeleteContent.cs | 59 ++++ .../Features/Contents/Handlers/GetContent.cs | 2 + .../Contents/Handlers/GetContentsByCreator.cs | 5 +- .../Contents/Handlers/Models/ContentModel.cs | 2 + .../20240816212531_AddsSoftDelete.Designer.cs | 264 ++++++++++++++++++ .../20240816212531_AddsSoftDelete.cs | 43 +++ .../ContentDbContextModelSnapshot.cs | 6 + 8 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 src/Web/Features/Contents/Handlers/DeleteContent.cs create mode 100644 src/Web/Features/Contents/Migrations/20240816212531_AddsSoftDelete.Designer.cs create mode 100644 src/Web/Features/Contents/Migrations/20240816212531_AddsSoftDelete.cs diff --git a/src/Web/Features/Contents/Data/Content.cs b/src/Web/Features/Contents/Data/Content.cs index dff459a..3e5d03a 100644 --- a/src/Web/Features/Contents/Data/Content.cs +++ b/src/Web/Features/Contents/Data/Content.cs @@ -8,6 +8,8 @@ public class Content public Guid CreatedBy { get; init; } public Creator? Creator { get; set; } 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; } diff --git a/src/Web/Features/Contents/Handlers/DeleteContent.cs b/src/Web/Features/Contents/Handlers/DeleteContent.cs new file mode 100644 index 0000000..eb5b512 --- /dev/null +++ b/src/Web/Features/Contents/Handlers/DeleteContent.cs @@ -0,0 +1,59 @@ +using Hutopy.Web.Common; +using Hutopy.Web.Features.Contents.Data; + +namespace Hutopy.Web.Features.Contents.Handlers; + +[PublicAPI] +public record DeleteContentRequest( + Guid ContentId); + +[PublicAPI] +public sealed class DeleteContentRequestValidator : Validator +{ + public DeleteContentRequestValidator() + { + RuleFor(r => r.ContentId) + .NotNull().WithMessage("You should specify the ContentId") + .NotEmpty().WithMessage("You should specify a valid/not empty ContentId"); + } +} + +public sealed class DeleteContent( + ContentDbContext context) + : Endpoint +{ + public override void Configure() + { + Delete("/api/contents/{ContentId}"); + Options(o => o.WithTags("Contents")); + } + + public override async Task HandleAsync( + DeleteContentRequest req, + CancellationToken ct) + { + var content = await context.Contents.FindAsync( + [req.ContentId], + ct); + + if (content is null) + { + await SendNotFoundAsync(ct); + return; + } + + var userId = HttpContext.User.GetUserId(); + if (content.CreatedBy != userId) + { + await SendForbiddenAsync(ct); + return; + } + + content.DeletedAt = DateTimeOffset.UtcNow; + content.DeletedBy = userId; + + await context.SaveChangesAsync(ct); + + await SendOkAsync(ct); + } +} diff --git a/src/Web/Features/Contents/Handlers/GetContent.cs b/src/Web/Features/Contents/Handlers/GetContent.cs index a097020..3fe1c38 100644 --- a/src/Web/Features/Contents/Handlers/GetContent.cs +++ b/src/Web/Features/Contents/Handlers/GetContent.cs @@ -36,6 +36,8 @@ public class GetContent( 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, diff --git a/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs b/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs index c9ffb50..6392ee3 100644 --- a/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs +++ b/src/Web/Features/Contents/Handlers/GetContentsByCreator.cs @@ -37,6 +37,8 @@ public class GetContentsByCreatorHandler( c."Menu" as ColorMenu, c."Accent" as ColorAccent, content."CreatedAt", + content."DeletedBy", + content."DeletedAt", content."Title", content."Description", content."Urls" @@ -44,7 +46,8 @@ public class GetContentsByCreatorHandler( 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}' + WHERE content."CreatedBy" = '{req.CreatorId}' + AND content."DeletedBy" IS NULL """); if (req.LastId.HasValue) diff --git a/src/Web/Features/Contents/Handlers/Models/ContentModel.cs b/src/Web/Features/Contents/Handlers/Models/ContentModel.cs index f6cff02..c3cecc9 100644 --- a/src/Web/Features/Contents/Handlers/Models/ContentModel.cs +++ b/src/Web/Features/Contents/Handlers/Models/ContentModel.cs @@ -10,6 +10,8 @@ public class ContentModel public required string? ColorMenu { get; init; } public required string? ColorAccent { get; init; } public required DateTimeOffset CreatedAt { get; init; } + public Guid? DeletedBy { get; init; } + public DateTimeOffset? DeletedAt { get; init; } public required string Title { get; init; } public required string Description { get; init; } public required string[]? Urls { get; init; } diff --git a/src/Web/Features/Contents/Migrations/20240816212531_AddsSoftDelete.Designer.cs b/src/Web/Features/Contents/Migrations/20240816212531_AddsSoftDelete.Designer.cs new file mode 100644 index 0000000..4ffcce3 --- /dev/null +++ b/src/Web/Features/Contents/Migrations/20240816212531_AddsSoftDelete.Designer.cs @@ -0,0 +1,264 @@ +// +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("20240816212531_AddsSoftDelete")] + partial class AddsSoftDelete + { + /// + 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("DeletedBy") + .HasColumnType("uuid"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + 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.Navigation("Creator"); + }); + + 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/20240816212531_AddsSoftDelete.cs b/src/Web/Features/Contents/Migrations/20240816212531_AddsSoftDelete.cs new file mode 100644 index 0000000..a6b528d --- /dev/null +++ b/src/Web/Features/Contents/Migrations/20240816212531_AddsSoftDelete.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Hutopy.Web.Features.Contents.Migrations +{ + /// + public partial class AddsSoftDelete : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeletedBy", + schema: "Content", + table: "Contents", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "DeletedAt", + schema: "Content", + table: "Contents", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DeletedBy", + schema: "Content", + table: "Contents"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + schema: "Content", + table: "Contents"); + } + } +} diff --git a/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs b/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs index 245416b..e4821b9 100644 --- a/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs +++ b/src/Web/Features/Contents/Migrations/ContentDbContextModelSnapshot.cs @@ -37,6 +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("Description") .IsRequired() .HasMaxLength(2048)