-
Notifications
You must be signed in to change notification settings - Fork 0
feat(product): T3 — CRUD API controller + JWT authentication #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: devin/product-t2-persistence
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,218 @@ | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.EntityFrameworkCore; | ||
| using Product.Domain.DTOs; | ||
| using Product.Infrastructure.Data; | ||
|
|
||
| namespace Product.API.Controllers; | ||
|
|
||
| [ApiController] | ||
| [Route("api/[controller]")] | ||
| [Authorize] | ||
| public class ProductController : ControllerBase | ||
| { | ||
| private readonly ILogger<ProductController> _logger; | ||
| private readonly ProductDbContext _db; | ||
|
|
||
| public ProductController(ILogger<ProductController> logger) | ||
| public ProductController(ProductDbContext db) | ||
| { | ||
| _logger = logger; | ||
| _db = db; | ||
| } | ||
|
|
||
| [HttpGet] | ||
| public IActionResult GetAll() | ||
| [ProducesResponseType(typeof(IEnumerable<ProductDto>), StatusCodes.Status200OK)] | ||
| public async Task<IActionResult> GetAll() | ||
| { | ||
| // TODO: Implement — migrate logic from monolith's ProductController | ||
| return Ok(new { service = "Product", status = "scaffold" }); | ||
| var products = await _db.Products | ||
| .Include(p => p.ProductCategory) | ||
| .Select(p => new ProductDto | ||
| { | ||
| Id = p.Id, | ||
| Name = p.Name, | ||
| Description = p.Description, | ||
| Icon = p.Icon, | ||
| BuyingPrice = p.BuyingPrice, | ||
| SellingPrice = p.SellingPrice, | ||
| UnitsInStock = p.UnitsInStock, | ||
| IsActive = p.IsActive, | ||
| IsDiscontinued = p.IsDiscontinued, | ||
| ProductCategoryName = p.ProductCategory.Name | ||
| }) | ||
| .ToListAsync(); | ||
|
|
||
| return Ok(products); | ||
| } | ||
|
|
||
| [HttpGet("{id}")] | ||
| public IActionResult GetById(int id) | ||
| [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)] | ||
| [ProducesResponseType(StatusCodes.Status404NotFound)] | ||
| public async Task<IActionResult> GetById(int id) | ||
| { | ||
| var product = await _db.Products | ||
| .Include(p => p.ProductCategory) | ||
| .Where(p => p.Id == id) | ||
| .Select(p => new ProductDto | ||
| { | ||
| Id = p.Id, | ||
| Name = p.Name, | ||
| Description = p.Description, | ||
| Icon = p.Icon, | ||
| BuyingPrice = p.BuyingPrice, | ||
| SellingPrice = p.SellingPrice, | ||
| UnitsInStock = p.UnitsInStock, | ||
| IsActive = p.IsActive, | ||
| IsDiscontinued = p.IsDiscontinued, | ||
| ProductCategoryName = p.ProductCategory.Name | ||
| }) | ||
| .FirstOrDefaultAsync(); | ||
|
|
||
| if (product is null) return NotFound(); | ||
| return Ok(product); | ||
| } | ||
|
|
||
| [HttpPost] | ||
| [ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)] | ||
| public async Task<IActionResult> Create([FromBody] CreateProductRequest request) | ||
| { | ||
| var category = await _db.ProductCategories.FindAsync(request.ProductCategoryId); | ||
| if (category is null) return BadRequest("Invalid ProductCategoryId"); | ||
|
|
||
| var entity = new Product.Domain.Entities.Product | ||
| { | ||
| Name = request.Name, | ||
| Description = request.Description, | ||
| Icon = request.Icon, | ||
| BuyingPrice = request.BuyingPrice, | ||
| SellingPrice = request.SellingPrice, | ||
| UnitsInStock = request.UnitsInStock, | ||
| IsActive = request.IsActive, | ||
| IsDiscontinued = request.IsDiscontinued, | ||
| ParentId = request.ParentId, | ||
| ProductCategoryId = request.ProductCategoryId, | ||
| ProductCategory = category, | ||
| CreatedDate = DateTime.UtcNow, | ||
| UpdatedDate = DateTime.UtcNow | ||
| }; | ||
|
|
||
| _db.Products.Add(entity); | ||
| await _db.SaveChangesAsync(); | ||
|
|
||
| var dto = new ProductDto | ||
| { | ||
| Id = entity.Id, | ||
| Name = entity.Name, | ||
| Description = entity.Description, | ||
| Icon = entity.Icon, | ||
| BuyingPrice = entity.BuyingPrice, | ||
| SellingPrice = entity.SellingPrice, | ||
| UnitsInStock = entity.UnitsInStock, | ||
| IsActive = entity.IsActive, | ||
| IsDiscontinued = entity.IsDiscontinued, | ||
| ProductCategoryName = category.Name | ||
| }; | ||
|
|
||
| return CreatedAtAction(nameof(GetById), new { id = entity.Id }, dto); | ||
| } | ||
|
|
||
| [HttpPut("{id}")] | ||
| [ProducesResponseType(StatusCodes.Status204NoContent)] | ||
| [ProducesResponseType(StatusCodes.Status404NotFound)] | ||
| public async Task<IActionResult> Update(int id, [FromBody] CreateProductRequest request) | ||
| { | ||
| // TODO: Implement — migrate logic from monolith | ||
| return Ok(new { service = "Product", id }); | ||
| var entity = await _db.Products.FindAsync(id); | ||
| if (entity is null) return NotFound(); | ||
|
|
||
| var category = await _db.ProductCategories.FindAsync(request.ProductCategoryId); | ||
| if (category is null) return BadRequest("Invalid ProductCategoryId"); | ||
|
|
||
| entity.Name = request.Name; | ||
| entity.Description = request.Description; | ||
| entity.Icon = request.Icon; | ||
| entity.BuyingPrice = request.BuyingPrice; | ||
| entity.SellingPrice = request.SellingPrice; | ||
| entity.UnitsInStock = request.UnitsInStock; | ||
| entity.IsActive = request.IsActive; | ||
| entity.IsDiscontinued = request.IsDiscontinued; | ||
| entity.ParentId = request.ParentId; | ||
| entity.ProductCategoryId = request.ProductCategoryId; | ||
| entity.UpdatedDate = DateTime.UtcNow; | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
|
|
||
| await _db.SaveChangesAsync(); | ||
| return NoContent(); | ||
| } | ||
|
|
||
| [HttpDelete("{id}")] | ||
| [ProducesResponseType(StatusCodes.Status204NoContent)] | ||
| [ProducesResponseType(StatusCodes.Status404NotFound)] | ||
| public async Task<IActionResult> Delete(int id) | ||
| { | ||
| var entity = await _db.Products.FindAsync(id); | ||
| if (entity is null) return NotFound(); | ||
|
|
||
| _db.Products.Remove(entity); | ||
| await _db.SaveChangesAsync(); | ||
| return NoContent(); | ||
| } | ||
|
Comment on lines
+147
to
+154
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Delete endpoint does not handle FK constraint violation for parent products The Was this helpful? React with 👍 or 👎 to provide feedback.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Valid edge case. The |
||
|
|
||
| [HttpGet("categories")] | ||
| [ProducesResponseType(typeof(IEnumerable<CategoryDto>), StatusCodes.Status200OK)] | ||
| public async Task<IActionResult> GetCategories() | ||
| { | ||
| var categories = await _db.ProductCategories | ||
| .Select(c => new CategoryDto | ||
| { | ||
| Id = c.Id, | ||
| Name = c.Name, | ||
| Description = c.Description, | ||
| Icon = c.Icon | ||
| }) | ||
| .ToListAsync(); | ||
|
|
||
| return Ok(categories); | ||
| } | ||
|
|
||
| [HttpPost("categories")] | ||
| [ProducesResponseType(typeof(CategoryDto), StatusCodes.Status201Created)] | ||
| public async Task<IActionResult> CreateCategory([FromBody] CreateCategoryRequest request) | ||
| { | ||
| var entity = new Product.Domain.Entities.ProductCategory | ||
| { | ||
| Name = request.Name, | ||
| Description = request.Description, | ||
| Icon = request.Icon, | ||
| CreatedDate = DateTime.UtcNow, | ||
| UpdatedDate = DateTime.UtcNow | ||
| }; | ||
|
|
||
| _db.ProductCategories.Add(entity); | ||
| await _db.SaveChangesAsync(); | ||
|
|
||
| var dto = new CategoryDto | ||
| { | ||
| Id = entity.Id, | ||
| Name = entity.Name, | ||
| Description = entity.Description, | ||
| Icon = entity.Icon | ||
| }; | ||
|
|
||
| return CreatedAtAction(nameof(GetCategories), dto); | ||
| } | ||
| } | ||
|
|
||
| public record CreateProductRequest( | ||
| string Name, | ||
| string? Description, | ||
| string? Icon, | ||
| decimal BuyingPrice, | ||
| decimal SellingPrice, | ||
| int UnitsInStock, | ||
| bool IsActive, | ||
| bool IsDiscontinued, | ||
| int? ParentId, | ||
| int ProductCategoryId | ||
| ); | ||
|
|
||
| public record CreateCategoryRequest( | ||
| string Name, | ||
| string? Description, | ||
| string? Icon | ||
| ); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Product service is the only one with auth; other services remain unprotected This PR adds Was this helpful? React with 👍 or 👎 to provide feedback.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentional — this PR is scoped to the Product microservice only (T3). Other services will get their own auth in their respective task PRs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚩 GetAll loads entire product table without pagination
The
GetAll()endpoint at line 23 fetches all products from the database without any pagination, filtering, or limit. As the product catalog grows, this will cause increasingly large memory allocations and slow response times. Consider adding pagination parameters (skip/takeor page-based) for production readiness.Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged — pagination would be a good follow-up. The current implementation matches the spec for this task (verbatim from the monolith migration). Pagination can be added in a future iteration when the catalog grows.