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
+ };
+ }
+}