From 70ee4e076e1ba933b9d7a301aba3bbae8bc714d1 Mon Sep 17 00:00:00 2001 From: Mate Farkas Date: Fri, 27 Dec 2024 09:45:53 +0100 Subject: [PATCH] feat: Implement Query endpoint for Connections puzzles --- Database/Repository/PuzzleRepository.cs | 37 ++++++++++-- Features/Connections/Query/Endpoint.cs | 18 +++++- Features/Puzzle/Get/GetPuzzleEndpoint.cs | 67 --------------------- Features/Puzzle/List/ListPuzzlesEndpoint.cs | 34 ----------- Models/Request/QueryPuzzlesRequest.cs | 3 + Models/Response/ConnectionsPuzzleDTO.cs | 16 ++--- Validators/QueryPuzzlesRequestValidator.cs | 9 +++ 7 files changed, 66 insertions(+), 118 deletions(-) delete mode 100644 Features/Puzzle/Get/GetPuzzleEndpoint.cs delete mode 100644 Features/Puzzle/List/ListPuzzlesEndpoint.cs diff --git a/Database/Repository/PuzzleRepository.cs b/Database/Repository/PuzzleRepository.cs index f2c5a54..1b7bc4b 100644 --- a/Database/Repository/PuzzleRepository.cs +++ b/Database/Repository/PuzzleRepository.cs @@ -1,4 +1,5 @@ using ConnectionsAPI.Database.Entities; +using ConnectionsAPI.Models.Response; using Microsoft.EntityFrameworkCore; namespace ConnectionsAPI.Database.Repository; @@ -32,12 +33,26 @@ public class PuzzleRepository(ConnectionsContext _db) return puzzle; } - public async Task> GetAllConnectionsAsync(bool includeSolutions = true) + public async Task> QueryConnectionsPuzzles(int page, + int pageCount, + int? year, + int? month, + bool includeSolutions = true) { - // query all, ordered by print date var query = _db.ConnectionsPuzzles .AsNoTracking(); + if (year != null) + { + string filterStr = $"{year}-%"; + if (month != null) + { + filterStr = $"{year}-{month.ToString()!.PadLeft(2, '0')}-%"; + } + + query = query.Where(x => EF.Functions.Like(x.PrintDate, filterStr)); + } + if (includeSolutions) { query = query @@ -45,14 +60,24 @@ public class PuzzleRepository(ConnectionsContext _db) .ThenInclude(x => x.Cards); } - var result = (await query.OrderBy(x => x.PrintDate).ToListAsync()) ?? []; + query = query + .OrderBy(x => x.PrintDate) + .Skip((page - 1) * pageCount) + .Take(pageCount); - foreach (var puzzle in result) + var puzzles = await query.ToListAsync(); + + if (puzzles.Count > 0) { - await EnhanceConnectionsWithDatesAsync(puzzle); + foreach (var puzzle in puzzles) + { + await EnhanceConnectionsWithDatesAsync(puzzle); + } } - return result; + int totalCount = await _db.ConnectionsPuzzles.AsNoTracking().CountAsync(); + + return new PagedDataResponse(page, puzzles.Count, totalCount, puzzles); } private async Task EnhanceConnectionsWithDatesAsync(ConnectionsPuzzle puzzle) diff --git a/Features/Connections/Query/Endpoint.cs b/Features/Connections/Query/Endpoint.cs index 40b9a98..198f983 100644 --- a/Features/Connections/Query/Endpoint.cs +++ b/Features/Connections/Query/Endpoint.cs @@ -1,17 +1,29 @@ +using ConnectionsAPI.Database.Repository; using ConnectionsAPI.Models.Request; using ConnectionsAPI.Models.Response; namespace ConnectionsAPI.Features.Connections.Query; -public class Endpoint : Endpoint> +public class Endpoint(PuzzleRepository _puzzleRepository) : Endpoint> { public override void Configure() { Get("query"); + Group(); } - public override Task HandleAsync(QueryPuzzlesRequest req, CancellationToken ct) + public override async Task HandleAsync(QueryPuzzlesRequest req, CancellationToken ct) { - return base.HandleAsync(req, ct); + bool hideSolutions = Query("hideSolutions", isRequired: false); + + var puzzles = await _puzzleRepository.QueryConnectionsPuzzles(req.Page, + req.Count, + req.Year, + req.Month, + !hideSolutions); + + PagedDataResponse response = new(puzzles.Page, puzzles.Count, puzzles.MaxCount, puzzles.Data.Select(ConnectionsPuzzleDTO.FromEntity).ToList()); + + await SendAsync(response, cancellation: ct); } } diff --git a/Features/Puzzle/Get/GetPuzzleEndpoint.cs b/Features/Puzzle/Get/GetPuzzleEndpoint.cs deleted file mode 100644 index 96c06fa..0000000 --- a/Features/Puzzle/Get/GetPuzzleEndpoint.cs +++ /dev/null @@ -1,67 +0,0 @@ -// using ConnectionsAPI.Database.Repository; -// using ConnectionsAPI.Models; -// using FluentValidation; -// using LazyCache; -// using System.Text.RegularExpressions; - -// namespace ConnectionsAPI.Features.Puzzle.Get -// { -// public record GetPuzzleEndpointRequest(string PuzzleDate); - -// public partial class GetPuzzleEndpointRequestValidator : Validator -// { -// [GeneratedRegex("^\\d{4}-\\d{2}-\\d{2}$", RegexOptions.IgnoreCase)] -// private static partial Regex PrintDateGeneratedRegex(); - -// public GetPuzzleEndpointRequestValidator() -// { -// RuleFor(x => x.PuzzleDate) -// .NotEmpty().WithMessage("Puzzle date is required") -// .Must(x => PrintDateGeneratedRegex().IsMatch(x)).WithMessage("Puzzle date must be in the format yyyy-MM-dd"); -// } -// } - -// public class GetPuzzleEndpoint(PuzzleRepository puzzleRepo, ILogger logger, IAppCache cache) : Endpoint -// { -// private readonly PuzzleRepository _puzzleRepo = puzzleRepo; -// private readonly ILogger _logger = logger; -// private readonly IAppCache _cache = cache; - -// public override void Configure() -// { -// Get("/{PuzzleDate}.json", -// "/puzzle/{PuzzleDate}"); -// AllowAnonymous(); -// DontThrowIfValidationFails(); -// } - -// public override async Task HandleAsync(GetPuzzleEndpointRequest req, CancellationToken ct) -// { -// // default to 404 if validation fails -// if (ValidationFailed) -// { -// _logger.LogError("Validation error. {path} {pathBase}", HttpContext.Request.Path, HttpContext.Request.PathBase); -// await SendNotFoundAsync(ct); -// return; -// } - -// bool hideSolutions = Query("hideSolutions", isRequired: false); - -// // query for the puzzle -// var puzzle = await _puzzleRepo.GetPuzzleByDateAsync(req.PuzzleDate, includeSolutions: !hideSolutions); - -// // if not found, done here -// if (puzzle == null) -// { -// await SendNotFoundAsync(ct); -// return; -// } - -// // get response from cache -// var response = ConnectionsPuzzleDTO.FromEntity(puzzle); - -// // done -// await SendAsync(response, cancellation: ct); -// } -// } -// } diff --git a/Features/Puzzle/List/ListPuzzlesEndpoint.cs b/Features/Puzzle/List/ListPuzzlesEndpoint.cs deleted file mode 100644 index 935c468..0000000 --- a/Features/Puzzle/List/ListPuzzlesEndpoint.cs +++ /dev/null @@ -1,34 +0,0 @@ -// using ConnectionsAPI.Database.Repository; -// using ConnectionsAPI.Models; -// using LazyCache; -// using Microsoft.EntityFrameworkCore; - -// namespace ConnectionsAPI.Features.Puzzle.List -// { -// public class ListPuzzlesEndpoint(PuzzleRepository puzzleRepo, IAppCache cache) : EndpointWithoutRequest> -// { -// private readonly PuzzleRepository _puzzleRepo = puzzleRepo; -// private readonly IAppCache _cache = cache; - -// public override void Configure() -// { -// Get("/all.json", -// "/puzzle/all"); -// AllowAnonymous(); -// } - -// public override async Task HandleAsync(CancellationToken ct) -// { -// bool hideSolutions = Query("hideSolutions", isRequired: false); - -// // query all, ordered by print date -// var puzzles = await _puzzleRepo.GetAllPuzzlesAsync(includeSolutions: !hideSolutions); - -// // map to response object -// var response = puzzles.Select(ConnectionsPuzzleDTO.FromEntity).ToList(); - -// // done -// await SendAsync(response, cancellation: ct); -// } -// } -// } diff --git a/Models/Request/QueryPuzzlesRequest.cs b/Models/Request/QueryPuzzlesRequest.cs index 881a24b..1a8b526 100644 --- a/Models/Request/QueryPuzzlesRequest.cs +++ b/Models/Request/QueryPuzzlesRequest.cs @@ -4,4 +4,7 @@ public class QueryPuzzlesRequest { [QueryParam] public int Page { get; set; } [QueryParam] public int Count { get; set; } + + [QueryParam] public int? Year { get; set; } + [QueryParam] public int? Month { get; set; } } diff --git a/Models/Response/ConnectionsPuzzleDTO.cs b/Models/Response/ConnectionsPuzzleDTO.cs index 90e0057..8330b92 100644 --- a/Models/Response/ConnectionsPuzzleDTO.cs +++ b/Models/Response/ConnectionsPuzzleDTO.cs @@ -11,7 +11,7 @@ public class ConnectionsPuzzleDTO NextPuzzle = dbPuzzle.NextPrintDate ?? string.Empty, PreviousPuzzle = dbPuzzle.PrevPrintDate ?? string.Empty, - Categories = dbPuzzle.Categories.OrderBy(x => (int)x.Color).Select(PuzzleCategoryDTO.FromEntity).ToList(), + Categories = dbPuzzle.Categories.OrderBy(x => (int)x.Color).Select(ConnectionsCategoryDTO.FromEntity).ToList(), }; public int PuzzleNumber { get; set; } @@ -19,16 +19,16 @@ public class ConnectionsPuzzleDTO public string PreviousPuzzle { get; set; } = string.Empty; public string NextPuzzle { get; set; } = string.Empty; public string Editor { get; set; } = string.Empty; - public ICollection Categories { get; set; } = []; + public ICollection Categories { get; set; } = []; } -public class PuzzleCategoryDTO +public class ConnectionsCategoryDTO { - public static PuzzleCategoryDTO FromEntity(Database.Entities.ConnectionsCategory dbCategory) => + public static ConnectionsCategoryDTO FromEntity(Database.Entities.ConnectionsCategory dbCategory) => new() { Title = dbCategory.Name, - Cards = dbCategory.Cards.OrderBy(x => x.Content).Select(PuzzleCardDTO.FromEntity).ToList(), + Cards = dbCategory.Cards.OrderBy(x => x.Content).Select(ConnectionsCardDTO.FromEntity).ToList(), Color = dbCategory.Color.ToString().ToLower(), OrderingKey = (int)dbCategory.Color }; @@ -36,12 +36,12 @@ public class PuzzleCategoryDTO public string Title { get; set; } = string.Empty; public string Color { get; set; } = string.Empty; public int OrderingKey { get; set; } - public ICollection Cards { get; set; } = []; + public ICollection Cards { get; set; } = []; } -public class PuzzleCardDTO +public class ConnectionsCardDTO { - public static PuzzleCardDTO FromEntity(Database.Entities.ConnectionsCard dbCard) => + public static ConnectionsCardDTO FromEntity(Database.Entities.ConnectionsCard dbCard) => new() { Content = dbCard.Content, diff --git a/Validators/QueryPuzzlesRequestValidator.cs b/Validators/QueryPuzzlesRequestValidator.cs index 27fda23..c606902 100644 --- a/Validators/QueryPuzzlesRequestValidator.cs +++ b/Validators/QueryPuzzlesRequestValidator.cs @@ -1,4 +1,5 @@ using System; +using System.IO.Compression; using ConnectionsAPI.Models.Request; using FluentValidation; @@ -15,5 +16,13 @@ public class QueryPuzzlesRequestValidator : Validator RuleFor(x => x.Count) .Must(x => x > 0) .WithMessage(x => "Item count must be a positive integer"); + + RuleFor(x => x.Year) + .Must(x => x == null || (x != null && x >= 2021 && x <= DateTime.UtcNow.Year + 1)) + .WithMessage($"Year must be a valid year between 2021 and {DateTime.UtcNow.Year + 1}"); + + RuleFor(x => x.Month) + .Must(x => x == null || (x != null && x >= 1 && x <= 12)) + .WithMessage("Month must be a valid month"); } }