using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using OnlyPrompt.Backend.ApiModels.Category; using OnlyPrompt.Backend.Database; using OnlyPrompt.Backend.Database.Models; using OnlyPrompt.Backend.Utils; using System.ComponentModel.DataAnnotations; namespace OnlyPrompt.Backend.Controllers { [ApiController] [Route("api/v1/categories")] [Authorize(Roles = ModelConstants.AdminRole, AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] public class CategoryController : BaseController { private static ValidationProblem SlugExistsProblem = TypedResults.ValidationProblem(new Dictionary { { nameof(CategoryModel.Slug), new[] { "Slug already exists." } } }); public CategoryController(OnlyPromptContext db, IMapper mapper) : base(db, mapper) { } [HttpGet("minimal")] [Authorize(Roles = ModelConstants.UserRole)] public async Task GetMinimalCategoriesAsync() { var categories = await _db.Categories .ProjectTo(_mapper.ConfigurationProvider) .ToArrayAsync(); return categories; } [HttpGet] [Authorize(Roles = ModelConstants.UserRole)] public async Task GetCategoriesAsync([Range(0, double.MaxValue)][FromQuery] int offset = 0, [Range(1, 100)][FromQuery] int limit = 20) { var categories = await _db.Categories .OrderBy(c => c.Id) .Skip(offset) .Take(limit) .ProjectTo(_mapper.ConfigurationProvider) .ToArrayAsync(); return categories; } [HttpPost] public async Task, ValidationProblem>> CreateCategoryAsync([FromBody] ApiCreateCategoryRequest request) { var exists = await _db.Categories.AnyAsync(c => c.Slug == request.Slug); if (exists) return SlugExistsProblem; var model = _mapper.Map(request); if (string.IsNullOrWhiteSpace(model.Slug)) model.Slug = await SlugHelper.GenerateUniqueSlugAsync(request.Name, slug => _db.Categories.AnyAsync(c => c.Slug == slug), ModelConstants.MaxSlugLength); _db.Categories.Add(model); await _db.SaveChangesAsync(); return TypedResults.Ok(_mapper.Map(model)); } [HttpPut("{id}")] public async Task, NotFound, ValidationProblem>> UpdateCategoryAsync(Identifier id, [FromBody] ApiUpdateCategoryRequest request) { var category = await _db.Categories.FindByIdentifierAsync(id); if (category is null) return TypedResults.NotFound("Category not found"); if (string.IsNullOrWhiteSpace(request.Name) == false) category.Name = request.Name; if(string.IsNullOrWhiteSpace(request.Description) == false) category.Description = request.Description; if (string.IsNullOrWhiteSpace(request.Slug) == false && request.Slug != category.Slug) { var exists = await _db.Categories.AnyAsync(c => c.Slug == request.Slug && c.Id != category.Id); if (exists) return SlugExistsProblem; category.Slug = request.Slug; } await _db.SaveChangesAsync(); return TypedResults.Ok(_mapper.Map(category)); } [HttpDelete("{id}")] public async Task, NotFound>> DeleteCategoryAsync(Identifier id, [FromQuery] Identifier? replaceWith = null) { var hasPrompts = await _db.Prompts.AnyAsync(p => p.CategoryId == id.Id); if (hasPrompts) { if (replaceWith.HasValue == false) return TypedResults.BadRequest("Category has associated prompts. Provide a replacement category to reassign them to."); var replacement = await _db.Categories.FindByIdentifierAsync(replaceWith.Value); if(replacement is null) return TypedResults.NotFound("Replacement category not found."); await _db.Prompts.Where(p => p.CategoryId == id.Id) .ExecuteUpdateAsync(p => p.SetProperty(p => p.CategoryId, replacement.Id)); } var count = await _db.Categories.OfIdentifer(id) .ExecuteDeleteAsync(); if (count == 0) return TypedResults.NotFound("Category not found"); return TypedResults.NoContent(); } } }