From 0a6633316c28f57be57b45dfba6e03ff619c0db4 Mon Sep 17 00:00:00 2001 From: eee4 <41441600+eee4@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:08:00 +0200 Subject: [PATCH 1/2] chore: migrate to new category structure --- DTOs/CategoryShortDTO.cs | 9 + Entities/Category.cs | 6 +- ...50718084441_more_category_data.Designer.cs | 190 ++++++++++++++++++ .../20250718084441_more_category_data.cs | 57 ++++++ .../ApplicationDbContextModelSnapshot.cs | 7 + 5 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 DTOs/CategoryShortDTO.cs create mode 100644 Migrations/20250718084441_more_category_data.Designer.cs create mode 100644 Migrations/20250718084441_more_category_data.cs diff --git a/DTOs/CategoryShortDTO.cs b/DTOs/CategoryShortDTO.cs new file mode 100644 index 0000000..9f41be3 --- /dev/null +++ b/DTOs/CategoryShortDTO.cs @@ -0,0 +1,9 @@ +namespace QuotifyBE.DTOs; +public record class CategoryShortDTO +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public DateTime? CreatedAt { get; set; } = DateTime.UtcNow; + +}; diff --git a/Entities/Category.cs b/Entities/Category.cs index b168608..b4c3b7d 100644 --- a/Entities/Category.cs +++ b/Entities/Category.cs @@ -1,8 +1,10 @@ -namespace QuotifyBE.Entities +namespace QuotifyBE.Entities { public class Category { public int Id { get; set; } - public string? Name { get; set; } + required public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public DateTime? CreatedAt { get; set; } = DateTime.UtcNow; } } diff --git a/Migrations/20250718084441_more_category_data.Designer.cs b/Migrations/20250718084441_more_category_data.Designer.cs new file mode 100644 index 0000000..12b3cc4 --- /dev/null +++ b/Migrations/20250718084441_more_category_data.Designer.cs @@ -0,0 +1,190 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using QuotifyBE.Data; + +#nullable disable + +namespace QuotifyBE.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250718084441_more_category_data")] + partial class more_category_data + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("QuotifyBE.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("QuotifyBE.Entities.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("QuotifyBE.Entities.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageId") + .HasColumnType("integer"); + + b.Property("LastUpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("UserId"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("QuotifyBE.Entities.QuoteCategory", b => + { + b.Property("QuoteId") + .HasColumnType("integer"); + + b.Property("CategoryId") + .HasColumnType("integer"); + + b.HasKey("QuoteId", "CategoryId"); + + b.HasIndex("CategoryId"); + + b.ToTable("QuoteCategories"); + }); + + modelBuilder.Entity("QuotifyBE.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("QuotifyBE.Entities.Quote", b => + { + b.HasOne("QuotifyBE.Entities.Image", "Image") + .WithMany() + .HasForeignKey("ImageId"); + + b.HasOne("QuotifyBE.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("QuotifyBE.Entities.QuoteCategory", b => + { + b.HasOne("QuotifyBE.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("QuotifyBE.Entities.Quote", "Quote") + .WithMany("QuoteCategories") + .HasForeignKey("QuoteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("Quote"); + }); + + modelBuilder.Entity("QuotifyBE.Entities.Quote", b => + { + b.Navigation("QuoteCategories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20250718084441_more_category_data.cs b/Migrations/20250718084441_more_category_data.cs new file mode 100644 index 0000000..7f8fcae --- /dev/null +++ b/Migrations/20250718084441_more_category_data.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace QuotifyBE.Migrations +{ + /// + public partial class more_category_data : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Name", + table: "Categories", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "CreatedAt", + table: "Categories", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "Description", + table: "Categories", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatedAt", + table: "Categories"); + + migrationBuilder.DropColumn( + name: "Description", + table: "Categories"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Categories", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + } +} diff --git a/Migrations/ApplicationDbContextModelSnapshot.cs b/Migrations/ApplicationDbContextModelSnapshot.cs index 34f29bc..6b415fa 100644 --- a/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Migrations/ApplicationDbContextModelSnapshot.cs @@ -30,7 +30,14 @@ namespace QuotifyBE.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + b.Property("Name") + .IsRequired() .HasColumnType("text"); b.HasKey("Id"); From b20b7d91270f7bd7a31201c8e5b614bac4ae146c Mon Sep 17 00:00:00 2001 From: eee4 <41441600+eee4@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:09:27 +0200 Subject: [PATCH 2/2] feat: basic category controller (create & retrieve) --- Controllers/CategoryController.cs | 111 ++++++++++++++++++++++++++++++ DTOs/NewCategoryDTO.cs | 6 ++ Mapping/CategoryMapping.cs | 19 +++++ 3 files changed, 136 insertions(+) create mode 100644 Controllers/CategoryController.cs create mode 100644 DTOs/NewCategoryDTO.cs create mode 100644 Mapping/CategoryMapping.cs diff --git a/Controllers/CategoryController.cs b/Controllers/CategoryController.cs new file mode 100644 index 0000000..ae1f520 --- /dev/null +++ b/Controllers/CategoryController.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using QuotifyBE.Data; +using QuotifyBE.Entities; +using QuotifyBE.DTOs; +using System.Threading.Tasks; +using QuotifyBE.Mapping; +using Microsoft.AspNetCore.Cors; +using Microsoft.EntityFrameworkCore; + +namespace QuotifyBE.Controllers; + + +[ApiController] +[EnableCors] +[Route("api/v1/categories")] +[Produces("application/json")] +public class CategoryController : ControllerBase +{ + + private readonly ApplicationDbContext _db; + private readonly GeneralUseHelpers guhf; + + public CategoryController(ApplicationDbContext db, GeneralUseHelpers GUHF) + { + _db = db; + guhf = GUHF; + } + + // GET /api/v1/categories + /// + /// Get every category + /// + /// + /// Can (and will) return an empty list if no categories are found in DB.
+ /// Has CORS set. + ///
+ /// Returned on valid request + // /// Returned when there are no categories to list + [HttpGet] + [EnableCors] + [ProducesResponseType(typeof(CategoryShortDTO), 200)] + // [ProducesResponseType(typeof(ErrorDTO), 404)] + public async Task GetQuotePage() + { + // The following seems to be a bad idea, so I leave it as is. ~eee4 + // + // int totalCategories = await _db.Categories.CountAsync(); + // + // if (totalCategories <= 0) + // { + // return NotFound(new ErrorDTO { Status = "error", Error_msg = "No categories to list" }); + // } + + // Get all the categories + List categories = await _db.Categories + .ToListAsync(); + + // Convert them to a list of DTO + List result = categories + .Select(c => c.ToCategoryShortDTO()) + .ToList(); + + // Return to user + return Ok(result); + + } + + // POST /api/v1/categories + /// + /// [AUTHED] Create a new category + /// + /// + /// Allows authorized users to create categories.
+ /// Important! Category names are case insensitive.
+ /// Has CORS set. + ///
+ /// Returned on valid request + /// Returned when such category already exists (case insensitive) + [HttpPost] + [Authorize] + [EnableCors] + [ProducesResponseType(typeof(CategoryShortDTO), 200)] + [ProducesResponseType(typeof(ErrorDTO), 406)] + public async Task PostNewCategory([FromBody] NewCategoryDTO formCategory) + { + // Check if such category doesn't already exist + Category? cat = await _db.Categories.FirstOrDefaultAsync(c => c.Name.ToLower() == formCategory.Name.ToLower()); + if (cat != null) + { + return StatusCode(406, new ErrorDTO { Status = "error", Error_msg = "This category already exists" }); + } + + // Create new category + cat = new Category + { + Name = formCategory.Name, + Description = formCategory.Description, + CreatedAt = DateTime.UtcNow + }; + + // Add to DB + await _db.Categories.AddAsync(cat); + await _db.SaveChangesAsync(); + + // And send back to the user as DTO + return Ok(cat.ToCategoryShortDTO()); + + } + +} diff --git a/DTOs/NewCategoryDTO.cs b/DTOs/NewCategoryDTO.cs new file mode 100644 index 0000000..7ac97e4 --- /dev/null +++ b/DTOs/NewCategoryDTO.cs @@ -0,0 +1,6 @@ +namespace QuotifyBE.DTOs; +public class NewCategoryDTO +{ + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } +} diff --git a/Mapping/CategoryMapping.cs b/Mapping/CategoryMapping.cs new file mode 100644 index 0000000..b4feaad --- /dev/null +++ b/Mapping/CategoryMapping.cs @@ -0,0 +1,19 @@ +using QuotifyBE.DTOs; +using QuotifyBE.Entities; + +namespace QuotifyBE.Mapping; + +public static class CategoryMapping +{ + public static CategoryShortDTO ToCategoryShortDTO(this Category category) + { + + return new CategoryShortDTO + { + Id = category.Id, + Name = category.Name, + Description = category.Description, + CreatedAt = category.CreatedAt + }; + } +}