using Butter.Dtos;
using Butter.Dtos.Tag;
using Lactose.Mapper;
using Lactose.Models;
using Lactose.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Lactose.Controllers;
//TODO: logging
///
/// Manages tags — hierarchical categorization of assets.
///
/// Tag repository.
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class TagController(
//TODO: add logging
//ILogger logger,
ITagRepository tagRepository
) : ControllerBase {
///
/// Gets a tag by its ID, optionally including its children and ancestors.
///
/// The tag ID.
/// Whether to include child tags.
/// Whether to include ancestor tags.
/// The tag with its hierarchy, or 404 if not found.
[HttpGet("{id}")]
public ActionResult Get(Guid id, bool children = false, bool ancestors = false) {
Tag? tag = tagRepository.Find(id);
if (tag == null) { return NotFound(); }
var tagDto = TagsMapper.ToTagDto(tag);
tagDto.Parent = tag.Parent == null ? null : TagsMapper.ToTagDto(tag.Parent);
if (children) {
tagDto.Children = [];
tagRepository.GetChildren(tag.Id).ForEach(t => tagDto.Children.Add(TagsMapper.ToTagDto(t)));
}
//TODO: Protect from overflow
if (ancestors) {
Tag? index = tag.Parent;
TagDto? indexDto = tagDto;
List ancestorsTags = [];
// This is redundant, because circular references shouldn't be present
while (index != null && !ancestorsTags.Contains(tag)) {
ancestorsTags.Add(index);
indexDto.Parent = TagsMapper.ToTagDto(index);
index = index.Parent;
indexDto = indexDto.Parent;
}
}
return tagDto;
}
///
/// Searches tags with pagination and optional search term.
///
/// Pagination and search parameters.
/// A list of tags matching the search criteria.
[HttpGet]
public ActionResult> GetAll([FromQuery] PagedSearchParametersDto pagedSearch) {
pagedSearch.Search ??= string.Empty;
if(pagedSearch.Page < 0) { return BadRequest("Page must be greater than 0"); }
if(pagedSearch.PageSize < 1 || pagedSearch.PageSize > PagedParametersDto.MaxPageSize) { return BadRequest($"PageSize must be between 1 and {PagedParametersDto.MaxPageSize}"); }
List tags = tagRepository.Search(pagedSearch.Search, pagedSearch.Page, pagedSearch.PageSize);
List tagsDto = [];
tags.ForEach(
t => {
var tagDto = TagsMapper.ToTagDto(t);
tagDto.Parent = t.Parent == null ? null : TagsMapper.ToTagDto(t.Parent);
tagsDto.Add(tagDto);
}
);
return tagsDto;
}
///
/// Updates an existing tag.
///
/// The tag ID.
/// The updated tag data.
/// 200 on success, 404 if not found, 409 if name conflict.
[Authorize(Roles = "Maintainer,Curator,Admin")]
[HttpPost("{id}")]
public IStatusCodeActionResult Update([FromRoute] Guid id, [FromBody] TagUpdateDto tagDto) {
IStatusCodeActionResult result = UpdateTag(id, tagDto.Name, tagDto.Parent);
if (result.StatusCode == StatusCodes.Status200OK) { tagRepository.Save(); }
return result;
}
///
/// Updates multiple tags in a bulk operation.
///
/// The bulk update data with IDs and shared update values.
/// 200 on success, or the first error response.
[Authorize(Roles = "Maintainer,Curator,Admin")]
[HttpPost]
public IStatusCodeActionResult BulkUpdate([FromBody] BulkDto tagBulkDto) {
foreach (Guid id in tagBulkDto.Ids) {
IStatusCodeActionResult result = UpdateTag(id, tagBulkDto.Data.Name, tagBulkDto.Data.Parent);
if (result.StatusCode != StatusCodes.Status200OK) { return result; }
}
tagRepository.Save();
return Ok();
}
///
/// Deletes a tag by its ID (soft delete).
///
/// The tag ID.
/// 200 on success, 404 if not found.
[Authorize(Roles = "Maintainer,Curator,Admin")]
[HttpDelete("{id}")]
public ActionResult Delete([FromRoute] Guid id) {
IStatusCodeActionResult result = DeleteTag(id);
if (result.StatusCode == StatusCodes.Status200OK) { tagRepository.Save(); }
return (ActionResult)result;
}
///
/// Deletes multiple tags in a bulk operation.
///
/// The list of tag IDs to delete.
/// 200 on success, or the first error response.
[Authorize(Roles = "Maintainer,Curator,Admin")]
[HttpDelete]
public IActionResult BulkDelete([FromBody] List ids) {
foreach (Guid id in ids) {
IStatusCodeActionResult result = DeleteTag(id);
if (result.StatusCode != StatusCodes.Status200OK) { return result; }
}
tagRepository.Save();
return Ok();
}
///
/// Creates a new tag.
///
/// The tag creation data.
/// 200 on success, 409 if a tag with the same name already exists, 400 if parent not found.
[Authorize(Roles = "Maintainer,Curator,Admin")]
[HttpPut]
public ActionResult Create([FromBody] TagCreateDto tagDto) {
if (tagRepository.FindByName(tagDto.Name) != null) { return Conflict($"Tag with name {tagDto.Name} already exist"); }
var tag = new Tag() {
Name = tagDto.Name
};
if (tagDto.Parent != null && tagRepository.Find(tagDto.Parent.Value) == null) {
return BadRequest($"No Tag with id {tagDto.Parent} found");
}
tag.ParentId = tagDto.Parent;
tagRepository.Insert(tag);
tagRepository.Save();
return Ok();
}
IStatusCodeActionResult DeleteTag(Guid id) {
Tag? tag = tagRepository.Find(id);
if (tag == null) { return NotFound($"Tag {id} not found"); }
tagRepository.Delete(tag);
return Ok();
}
IStatusCodeActionResult UpdateTag(Guid id, string? name, Guid? parentId) {
Tag? tag = tagRepository.Find(id);
if (tag == null) { return NotFound($"Tag {id} not found"); }
if (name != null) {
Tag? foundTag = tagRepository.FindByName(name);
if (foundTag != null && foundTag.Id != tag.Id) { return Conflict($"Tag with name '{name}' already exists"); }
tag.Name = name;
}
if (parentId == null) { return Ok(); }
if (parentId.Value.Equals(Guid.Empty)) { tag.Parent = null; } else {
Tag? parent = tagRepository.Find(parentId.Value);
if (parent == null) { return NotFound($"Parent tag on {id} was not found"); }
tag.Parent = parent;
}
return Ok();
}
}