diff --git a/Database/ConnectionsContext.cs b/Database/ConnectionsContext.cs index d0e5597..1a4e743 100644 --- a/Database/ConnectionsContext.cs +++ b/Database/ConnectionsContext.cs @@ -11,6 +11,11 @@ namespace ConnectionsAPI.Database { modelBuilder.Entity() .HasIndex(x => x.PrintDate).IsUnique(); + modelBuilder.Entity() + .Ignore(x => x.NextPrintDate); + modelBuilder.Entity() + .Ignore(x => x.PrevPrintDate); + base.OnModelCreating(modelBuilder); } diff --git a/Database/Entities/Puzzle.cs b/Database/Entities/Puzzle.cs index 5459440..305d138 100644 --- a/Database/Entities/Puzzle.cs +++ b/Database/Entities/Puzzle.cs @@ -1,4 +1,6 @@ -namespace ConnectionsAPI.Database.Entities +using System.ComponentModel.DataAnnotations.Schema; + +namespace ConnectionsAPI.Database.Entities { public class Puzzle { @@ -36,5 +38,10 @@ /// The categories associated with this puzzle /// public virtual ICollection Categories { get; set; } = []; + + [NotMapped] + public string? PrevPrintDate { get; set; } + [NotMapped] + public string? NextPrintDate { get; set; } } } diff --git a/Database/Repository/PuzzleRepository.cs b/Database/Repository/PuzzleRepository.cs new file mode 100644 index 0000000..44e0bbd --- /dev/null +++ b/Database/Repository/PuzzleRepository.cs @@ -0,0 +1,76 @@ +using ConnectionsAPI.Database.Entities; +using Microsoft.EntityFrameworkCore; + +namespace ConnectionsAPI.Database.Repository +{ + public class PuzzleRepository(ConnectionsContext _db) + { + private readonly ConnectionsContext _db = _db; + + public async Task GetPuzzleByDateAsync(string printDate, bool includeSolutions = true) + { + // query for the puzzle + var query = _db.Puzzles.AsNoTracking(); + + if (includeSolutions) + { + query = query + .Include(x => x.Categories) + .ThenInclude(x => x.PuzzleCards); + } + + var puzzle = await query.FirstOrDefaultAsync(x => x.PrintDate == printDate); + + // if not found, we're done here + if (puzzle == null) + { + return null; + } + + await EnhancePuzzleWithDatesAsync(puzzle); + + return puzzle; + } + + public async Task> GetAllPuzzlesAsync(bool includeSolutions = true) + { + // query all, ordered by print date + var query = _db.Puzzles + .AsNoTracking(); + + if (includeSolutions) + { + query = query + .Include(x => x.Categories) + .ThenInclude(x => x.PuzzleCards); + } + + var result = (await query.OrderBy(x => x.PrintDate).ToListAsync()) ?? []; + + foreach (var puzzle in result) + { + await EnhancePuzzleWithDatesAsync(puzzle); + } + + return result; + } + + private async Task EnhancePuzzleWithDatesAsync(Puzzle puzzle) + { + string? previousPuzzleDate = await _db.Puzzles.AsNoTracking() + .Where(x => x.PrintDate.CompareTo(puzzle.PrintDate) < 0) + .OrderByDescending(x => x.PrintDate) + .Select(x => x.PrintDate) + .FirstOrDefaultAsync(); + + string? nextPuzzleDate = await _db.Puzzles.AsNoTracking() + .Where(x => x.PrintDate.CompareTo(puzzle.PrintDate) > 0) + .OrderBy(x => x.PrintDate) + .Select(x => x.PrintDate) + .FirstOrDefaultAsync(); + + puzzle.PrevPrintDate = previousPuzzleDate; + puzzle.NextPrintDate = nextPuzzleDate; + } + } +} diff --git a/Features/Puzzle/Get/GetPuzzleEndpoint.cs b/Features/Puzzle/Get/GetPuzzleEndpoint.cs index 47ff124..7a4459d 100644 --- a/Features/Puzzle/Get/GetPuzzleEndpoint.cs +++ b/Features/Puzzle/Get/GetPuzzleEndpoint.cs @@ -1,8 +1,7 @@ -using ConnectionsAPI.Database; +using ConnectionsAPI.Database.Repository; using ConnectionsAPI.Models; using FluentValidation; using LazyCache; -using Microsoft.EntityFrameworkCore; using System.Text.RegularExpressions; namespace ConnectionsAPI.Features.Puzzle.Get @@ -22,15 +21,16 @@ namespace ConnectionsAPI.Features.Puzzle.Get } } - public class GetPuzzleEndpoint(ConnectionsContext db, ILogger logger, IAppCache cache) : Endpoint + public class GetPuzzleEndpoint(PuzzleRepository puzzleRepo, ILogger logger, IAppCache cache) : Endpoint { - private readonly ConnectionsContext _db = db; + private readonly PuzzleRepository _puzzleRepo = puzzleRepo; private readonly ILogger _logger = logger; private readonly IAppCache _cache = cache; public override void Configure() { - Get("/{PuzzleDate}.json"); + Get("/{PuzzleDate}.json", + "/puzzle/{PuzzleDate}"); AllowAnonymous(); DontThrowIfValidationFails(); } @@ -45,39 +45,23 @@ namespace ConnectionsAPI.Features.Puzzle.Get return; } - // get response from cache - var response = await _cache.GetOrAddAsync($"Puzzle:{req.PuzzleDate}", - () => { return GetResponseForCache(req.PuzzleDate); }, - DateTimeOffset.UtcNow.AddMinutes(5)); + 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 (response == null) + if (puzzle == null) { await SendNotFoundAsync(ct); return; } + // get response from cache + var response = PuzzleDTO.FromEntity(puzzle); + // done await SendAsync(response, cancellation: ct); } - - private async Task GetResponseForCache(string printDate) - { - // query for the puzzle - var puzzle = await _db.Puzzles - .Include(x => x.Categories) - .ThenInclude(x => x.PuzzleCards) - .AsNoTracking() - .FirstOrDefaultAsync(x => x.PrintDate == printDate); - - // if not found, we're done here - if (puzzle == null) - { - return null; - } - - // if found, map - return PuzzleDTO.FromEntity(puzzle); - } } } diff --git a/Features/Puzzle/List/ListPuzzlesEndpoint.cs b/Features/Puzzle/List/ListPuzzlesEndpoint.cs index f1857e4..8fa6b6e 100644 --- a/Features/Puzzle/List/ListPuzzlesEndpoint.cs +++ b/Features/Puzzle/List/ListPuzzlesEndpoint.cs @@ -1,43 +1,34 @@ -using ConnectionsAPI.Database; +using ConnectionsAPI.Database.Repository; using ConnectionsAPI.Models; using LazyCache; using Microsoft.EntityFrameworkCore; namespace ConnectionsAPI.Features.Puzzle.List { - public class ListPuzzlesEndpoint(ConnectionsContext db, IAppCache cache) : EndpointWithoutRequest> + public class ListPuzzlesEndpoint(PuzzleRepository puzzleRepo, IAppCache cache) : EndpointWithoutRequest> { - private readonly ConnectionsContext _db = db; + private readonly PuzzleRepository _puzzleRepo = puzzleRepo; private readonly IAppCache _cache = cache; public override void Configure() { - Get("/all.json"); + Get("/all.json", + "/puzzle/all"); AllowAnonymous(); } public override async Task HandleAsync(CancellationToken ct) { - // get response from cache - var response = await _cache.GetOrAddAsync("Puzzle:All", - GetResponseForCache, - DateTimeOffset.UtcNow.AddMinutes(5)); + 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(PuzzleDTO.FromEntity).ToList(); + // done await SendAsync(response, cancellation: ct); } - - private async Task> GetResponseForCache() - { - // query all, ordered by print date - var puzzles = await _db.Puzzles - .Include(x => x.Categories) - .ThenInclude(x => x.PuzzleCards) - .AsNoTracking() - .OrderBy(x => x.PrintDate) - .ToListAsync(); - - // map to dto - return puzzles.Select(PuzzleDTO.FromEntity).ToList(); - } } } diff --git a/Models/PuzzleDTO.cs b/Models/PuzzleDTO.cs index d3221d5..0d94a7b 100644 --- a/Models/PuzzleDTO.cs +++ b/Models/PuzzleDTO.cs @@ -8,12 +8,18 @@ PuzzleNumber = dbPuzzle.Index, PrintDate = dbPuzzle.PrintDate, Editor = dbPuzzle.EditorName, - Categories = dbPuzzle.Categories.OrderBy(x => (int)x.Color).Select(PuzzleCategoryDTO.FromEntity).ToList() + + NextPuzzle = dbPuzzle.NextPrintDate ?? string.Empty, + PreviousPuzzle = dbPuzzle.PrevPrintDate ?? string.Empty, + + Categories = dbPuzzle.Categories.OrderBy(x => (int)x.Color).Select(PuzzleCategoryDTO.FromEntity).ToList(), }; public int PuzzleNumber { get; set; } public string PrintDate { get; set; } = string.Empty; - public string Editor { get; set;} = string.Empty; + 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; } = []; } diff --git a/Program.cs b/Program.cs index 176ac7f..b1add08 100644 --- a/Program.cs +++ b/Program.cs @@ -1,5 +1,6 @@ using ConnectionsAPI.Config; using ConnectionsAPI.Database; +using ConnectionsAPI.Database.Repository; using ConnectionsAPI.Utility; using Microsoft.EntityFrameworkCore; using System.Net; @@ -8,6 +9,8 @@ namespace ConnectionsAPI { public class Program { + const string CorsPolicyName = "DefaultCorsPolicy"; + public static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); @@ -36,8 +39,21 @@ namespace ConnectionsAPI builder.Services.AddLazyCache(); + builder.Services.AddScoped(); + builder.Services.AddHostedService(); + // configure clors + builder.Services.AddCors(config => + { + config.AddPolicy(CorsPolicyName, policy => + { + policy.AllowAnyOrigin(); + policy.AllowAnyHeader(); + policy.WithMethods("GET"); + }); + }); + var app = builder.Build(); var logger = app.Services.GetRequiredService>(); @@ -88,6 +104,9 @@ namespace ConnectionsAPI } } + // enable cors + app.UseCors(CorsPolicyName); + app.Run(); } } diff --git a/appsettings.Development.json b/appsettings.Development.json index 3169818..8123359 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -9,6 +9,6 @@ "ConnectionsContext": "Data Source=c:\\tmp\\connections-api\\dev.db;" }, "Sync": { - "RunImmediately": false + "RunImmediately": true } }