diff --git a/Database/ConnectionsContext.cs b/Database/ConnectionsContext.cs index 1a4e743..43870f9 100644 --- a/Database/ConnectionsContext.cs +++ b/Database/ConnectionsContext.cs @@ -5,15 +5,15 @@ namespace ConnectionsAPI.Database { public class ConnectionsContext(DbContextOptions dbContextOptions) : DbContext(dbContextOptions) { - public DbSet Puzzles { get; set; } + public DbSet CategoriesPuzzles { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity() + modelBuilder.Entity() .HasIndex(x => x.PrintDate).IsUnique(); - modelBuilder.Entity() + modelBuilder.Entity() .Ignore(x => x.NextPrintDate); - modelBuilder.Entity() + modelBuilder.Entity() .Ignore(x => x.PrevPrintDate); diff --git a/Database/Entities/PuzzleCard.cs b/Database/Entities/CategoriesCard.cs similarity index 81% rename from Database/Entities/PuzzleCard.cs rename to Database/Entities/CategoriesCard.cs index 3362f45..2ef78c6 100644 --- a/Database/Entities/PuzzleCard.cs +++ b/Database/Entities/CategoriesCard.cs @@ -1,12 +1,12 @@ namespace ConnectionsAPI.Database.Entities { - public class PuzzleCard + public class CategoriesCard { /// /// Primary key of the entity /// public int Id { get; set; } - + /// /// The contents of this card (the word) /// @@ -20,11 +20,11 @@ /// /// The ID of the associated Connections category /// - public int PuzzleCategoryId { get; set; } + public int CategoriesCategoryId { get; set; } /// /// The associated category instance /// - public virtual PuzzleCategory? PuzzleCategory { get; set; } + public virtual CategoriesCategory? Category { get; set; } } } \ No newline at end of file diff --git a/Database/Entities/PuzzleCategory.cs b/Database/Entities/CategoriesCategory.cs similarity index 72% rename from Database/Entities/PuzzleCategory.cs rename to Database/Entities/CategoriesCategory.cs index caf924b..e276515 100644 --- a/Database/Entities/PuzzleCategory.cs +++ b/Database/Entities/CategoriesCategory.cs @@ -1,6 +1,6 @@ namespace ConnectionsAPI.Database.Entities { - public class PuzzleCategory + public class CategoriesCategory { /// /// Primary key of the entity @@ -15,21 +15,21 @@ /// /// The color of the category in this Connections puzzle; Also used for sorting /// - public PuzzleCategoryColor Color { get; set; } + public CategoriesColor Color { get; set; } /// /// The ID of the associated Connections puzzle /// - public int PuzzleId { get; set; } - + public int CategoriesPuzzleId { get; set; } + /// /// The associated puzzle instance /// - public virtual Puzzle? Puzzle { get; set; } + public virtual CategoriesPuzzle? CategoriesPuzzle { get; set; } /// /// The cards associated with this category /// - public ICollection PuzzleCards { get; set; } = []; + public ICollection CategoriesPuzzleCards { get; set; } = []; } } diff --git a/Database/Entities/PuzzleCategoryColor.cs b/Database/Entities/CategoriesColor.cs similarity index 79% rename from Database/Entities/PuzzleCategoryColor.cs rename to Database/Entities/CategoriesColor.cs index e052586..2483343 100644 --- a/Database/Entities/PuzzleCategoryColor.cs +++ b/Database/Entities/CategoriesColor.cs @@ -1,6 +1,6 @@ namespace ConnectionsAPI.Database.Entities { - public enum PuzzleCategoryColor + public enum CategoriesColor { Yellow = 1, Green = 2, diff --git a/Database/Entities/Puzzle.cs b/Database/Entities/CategoriesPuzzle.cs similarity index 91% rename from Database/Entities/Puzzle.cs rename to Database/Entities/CategoriesPuzzle.cs index 305d138..b2b1a1b 100644 --- a/Database/Entities/Puzzle.cs +++ b/Database/Entities/CategoriesPuzzle.cs @@ -2,7 +2,7 @@ namespace ConnectionsAPI.Database.Entities { - public class Puzzle + public class CategoriesPuzzle { /// /// Primary key of the entity @@ -37,7 +37,7 @@ namespace ConnectionsAPI.Database.Entities /// /// The categories associated with this puzzle /// - public virtual ICollection Categories { get; set; } = []; + public virtual ICollection Categories { get; set; } = []; [NotMapped] public string? PrevPrintDate { get; set; } diff --git a/Database/Migrations/20241226082514_RenameCategoriesPuzzles.Designer.cs b/Database/Migrations/20241226082514_RenameCategoriesPuzzles.Designer.cs new file mode 100644 index 0000000..2835665 --- /dev/null +++ b/Database/Migrations/20241226082514_RenameCategoriesPuzzles.Designer.cs @@ -0,0 +1,135 @@ +// +using System; +using ConnectionsAPI.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ConnectionsAPI.Database.Migrations +{ + [DbContext(typeof(ConnectionsContext))] + [Migration("20241226082514_RenameCategoriesPuzzles")] + partial class RenameCategoriesPuzzles + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoriesCategoryId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoriesCategoryId"); + + b.ToTable("CategoriesCard"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoriesPuzzleId") + .HasColumnType("INTEGER"); + + b.Property("Color") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoriesPuzzleId"); + + b.ToTable("CategoriesCategory"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesPuzzle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentMD5") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedDate") + .HasColumnType("TEXT"); + + b.Property("EditorName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("PrintDate") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PrintDate") + .IsUnique(); + + b.ToTable("CategoriesPuzzles"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCard", b => + { + b.HasOne("ConnectionsAPI.Database.Entities.CategoriesCategory", "Category") + .WithMany("CategoriesPuzzleCards") + .HasForeignKey("CategoriesCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b => + { + b.HasOne("ConnectionsAPI.Database.Entities.CategoriesPuzzle", "CategoriesPuzzle") + .WithMany("Categories") + .HasForeignKey("CategoriesPuzzleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CategoriesPuzzle"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b => + { + b.Navigation("CategoriesPuzzleCards"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesPuzzle", b => + { + b.Navigation("Categories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Database/Migrations/20241226082514_RenameCategoriesPuzzles.cs b/Database/Migrations/20241226082514_RenameCategoriesPuzzles.cs new file mode 100644 index 0000000..d7785b3 --- /dev/null +++ b/Database/Migrations/20241226082514_RenameCategoriesPuzzles.cs @@ -0,0 +1,187 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ConnectionsAPI.Database.Migrations +{ + /// + public partial class RenameCategoriesPuzzles : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PuzzleCard"); + + migrationBuilder.DropTable( + name: "PuzzleCategory"); + + migrationBuilder.DropTable( + name: "Puzzles"); + + migrationBuilder.CreateTable( + name: "CategoriesPuzzles", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + CreatedDate = table.Column(type: "TEXT", nullable: false), + PrintDate = table.Column(type: "TEXT", nullable: false), + EditorName = table.Column(type: "TEXT", nullable: false), + Index = table.Column(type: "INTEGER", nullable: false), + ContentMD5 = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CategoriesPuzzles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CategoriesCategory", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false), + Color = table.Column(type: "INTEGER", nullable: false), + CategoriesPuzzleId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CategoriesCategory", x => x.Id); + table.ForeignKey( + name: "FK_CategoriesCategory_CategoriesPuzzles_CategoriesPuzzleId", + column: x => x.CategoriesPuzzleId, + principalTable: "CategoriesPuzzles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CategoriesCard", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Content = table.Column(type: "TEXT", nullable: false), + Position = table.Column(type: "INTEGER", nullable: false), + CategoriesCategoryId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CategoriesCard", x => x.Id); + table.ForeignKey( + name: "FK_CategoriesCard_CategoriesCategory_CategoriesCategoryId", + column: x => x.CategoriesCategoryId, + principalTable: "CategoriesCategory", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CategoriesCard_CategoriesCategoryId", + table: "CategoriesCard", + column: "CategoriesCategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_CategoriesCategory_CategoriesPuzzleId", + table: "CategoriesCategory", + column: "CategoriesPuzzleId"); + + migrationBuilder.CreateIndex( + name: "IX_CategoriesPuzzles_PrintDate", + table: "CategoriesPuzzles", + column: "PrintDate", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CategoriesCard"); + + migrationBuilder.DropTable( + name: "CategoriesCategory"); + + migrationBuilder.DropTable( + name: "CategoriesPuzzles"); + + migrationBuilder.CreateTable( + name: "Puzzles", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ContentMD5 = table.Column(type: "TEXT", nullable: false), + CreatedDate = table.Column(type: "TEXT", nullable: false), + EditorName = table.Column(type: "TEXT", nullable: false), + Index = table.Column(type: "INTEGER", nullable: false), + PrintDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Puzzles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "PuzzleCategory", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PuzzleId = table.Column(type: "INTEGER", nullable: false), + Color = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PuzzleCategory", x => x.Id); + table.ForeignKey( + name: "FK_PuzzleCategory_Puzzles_PuzzleId", + column: x => x.PuzzleId, + principalTable: "Puzzles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PuzzleCard", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PuzzleCategoryId = table.Column(type: "INTEGER", nullable: false), + Content = table.Column(type: "TEXT", nullable: false), + Position = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PuzzleCard", x => x.Id); + table.ForeignKey( + name: "FK_PuzzleCard_PuzzleCategory_PuzzleCategoryId", + column: x => x.PuzzleCategoryId, + principalTable: "PuzzleCategory", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PuzzleCard_PuzzleCategoryId", + table: "PuzzleCard", + column: "PuzzleCategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_PuzzleCategory_PuzzleId", + table: "PuzzleCategory", + column: "PuzzleId"); + + migrationBuilder.CreateIndex( + name: "IX_Puzzles_PrintDate", + table: "Puzzles", + column: "PrintDate", + unique: true); + } + } +} diff --git a/Database/Migrations/ConnectionsContextModelSnapshot.cs b/Database/Migrations/ConnectionsContextModelSnapshot.cs index 357f0d0..1ae13a3 100644 --- a/Database/Migrations/ConnectionsContextModelSnapshot.cs +++ b/Database/Migrations/ConnectionsContextModelSnapshot.cs @@ -17,7 +17,53 @@ namespace ConnectionsAPI.Database.Migrations #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); - modelBuilder.Entity("ConnectionsAPI.Database.Entities.Puzzle", b => + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoriesCategoryId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Position") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoriesCategoryId"); + + b.ToTable("CategoriesCard"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoriesPuzzleId") + .HasColumnType("INTEGER"); + + b.Property("Color") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoriesPuzzleId"); + + b.ToTable("CategoriesCategory"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesPuzzle", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -46,86 +92,40 @@ namespace ConnectionsAPI.Database.Migrations b.HasIndex("PrintDate") .IsUnique(); - b.ToTable("Puzzles"); + b.ToTable("CategoriesPuzzles"); }); - modelBuilder.Entity("ConnectionsAPI.Database.Entities.PuzzleCard", b => + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCard", b => { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Content") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Position") - .HasColumnType("INTEGER"); - - b.Property("PuzzleCategoryId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PuzzleCategoryId"); - - b.ToTable("PuzzleCard"); - }); - - modelBuilder.Entity("ConnectionsAPI.Database.Entities.PuzzleCategory", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Color") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PuzzleId") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("PuzzleId"); - - b.ToTable("PuzzleCategory"); - }); - - modelBuilder.Entity("ConnectionsAPI.Database.Entities.PuzzleCard", b => - { - b.HasOne("ConnectionsAPI.Database.Entities.PuzzleCategory", "PuzzleCategory") - .WithMany("PuzzleCards") - .HasForeignKey("PuzzleCategoryId") + b.HasOne("ConnectionsAPI.Database.Entities.CategoriesCategory", "Category") + .WithMany("CategoriesPuzzleCards") + .HasForeignKey("CategoriesCategoryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("PuzzleCategory"); + b.Navigation("Category"); }); - modelBuilder.Entity("ConnectionsAPI.Database.Entities.PuzzleCategory", b => + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b => { - b.HasOne("ConnectionsAPI.Database.Entities.Puzzle", "Puzzle") + b.HasOne("ConnectionsAPI.Database.Entities.CategoriesPuzzle", "CategoriesPuzzle") .WithMany("Categories") - .HasForeignKey("PuzzleId") + .HasForeignKey("CategoriesPuzzleId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Puzzle"); + b.Navigation("CategoriesPuzzle"); }); - modelBuilder.Entity("ConnectionsAPI.Database.Entities.Puzzle", b => + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b => + { + b.Navigation("CategoriesPuzzleCards"); + }); + + modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesPuzzle", b => { b.Navigation("Categories"); }); - - modelBuilder.Entity("ConnectionsAPI.Database.Entities.PuzzleCategory", b => - { - b.Navigation("PuzzleCards"); - }); #pragma warning restore 612, 618 } } diff --git a/Database/Repository/PuzzleRepository.cs b/Database/Repository/PuzzleRepository.cs index 44e0bbd..fb970f6 100644 --- a/Database/Repository/PuzzleRepository.cs +++ b/Database/Repository/PuzzleRepository.cs @@ -7,16 +7,16 @@ namespace ConnectionsAPI.Database.Repository { private readonly ConnectionsContext _db = _db; - public async Task GetPuzzleByDateAsync(string printDate, bool includeSolutions = true) + public async Task GetPuzzleByDateAsync(string printDate, bool includeSolutions = true) { // query for the puzzle - var query = _db.Puzzles.AsNoTracking(); + var query = _db.CategoriesPuzzles.AsNoTracking(); if (includeSolutions) { query = query .Include(x => x.Categories) - .ThenInclude(x => x.PuzzleCards); + .ThenInclude(x => x.CategoriesPuzzleCards); } var puzzle = await query.FirstOrDefaultAsync(x => x.PrintDate == printDate); @@ -32,17 +32,17 @@ namespace ConnectionsAPI.Database.Repository return puzzle; } - public async Task> GetAllPuzzlesAsync(bool includeSolutions = true) + public async Task> GetAllPuzzlesAsync(bool includeSolutions = true) { // query all, ordered by print date - var query = _db.Puzzles + var query = _db.CategoriesPuzzles .AsNoTracking(); if (includeSolutions) { query = query .Include(x => x.Categories) - .ThenInclude(x => x.PuzzleCards); + .ThenInclude(x => x.CategoriesPuzzleCards); } var result = (await query.OrderBy(x => x.PrintDate).ToListAsync()) ?? []; @@ -55,15 +55,15 @@ namespace ConnectionsAPI.Database.Repository return result; } - private async Task EnhancePuzzleWithDatesAsync(Puzzle puzzle) + private async Task EnhancePuzzleWithDatesAsync(CategoriesPuzzle puzzle) { - string? previousPuzzleDate = await _db.Puzzles.AsNoTracking() + string? previousPuzzleDate = await _db.CategoriesPuzzles.AsNoTracking() .Where(x => x.PrintDate.CompareTo(puzzle.PrintDate) < 0) .OrderByDescending(x => x.PrintDate) .Select(x => x.PrintDate) .FirstOrDefaultAsync(); - string? nextPuzzleDate = await _db.Puzzles.AsNoTracking() + string? nextPuzzleDate = await _db.CategoriesPuzzles.AsNoTracking() .Where(x => x.PrintDate.CompareTo(puzzle.PrintDate) > 0) .OrderBy(x => x.PrintDate) .Select(x => x.PrintDate) diff --git a/Models/PuzzleDTO.cs b/Models/PuzzleDTO.cs index 0d94a7b..cb0d791 100644 --- a/Models/PuzzleDTO.cs +++ b/Models/PuzzleDTO.cs @@ -2,7 +2,7 @@ { public class PuzzleDTO { - public static PuzzleDTO FromEntity(Database.Entities.Puzzle dbPuzzle) => + public static PuzzleDTO FromEntity(Database.Entities.CategoriesPuzzle dbPuzzle) => new() { PuzzleNumber = dbPuzzle.Index, @@ -25,11 +25,11 @@ public class PuzzleCategoryDTO { - public static PuzzleCategoryDTO FromEntity(Database.Entities.PuzzleCategory dbCategory) => + public static PuzzleCategoryDTO FromEntity(Database.Entities.CategoriesCategory dbCategory) => new() { Title = dbCategory.Name, - Cards = dbCategory.PuzzleCards.OrderBy(x => x.Content).Select(PuzzleCardDTO.FromEntity).ToList(), + Cards = dbCategory.CategoriesPuzzleCards.OrderBy(x => x.Content).Select(PuzzleCardDTO.FromEntity).ToList(), Color = dbCategory.Color.ToString().ToLower(), OrderingKey = (int)dbCategory.Color }; @@ -42,7 +42,7 @@ public class PuzzleCardDTO { - public static PuzzleCardDTO FromEntity(Database.Entities.PuzzleCard dbCard) => + public static PuzzleCardDTO FromEntity(Database.Entities.CategoriesCard dbCard) => new() { Content = dbCard.Content, diff --git a/Utility/SyncUtility.cs b/Utility/SyncUtility.cs index b54b424..8e0d7fa 100644 --- a/Utility/SyncUtility.cs +++ b/Utility/SyncUtility.cs @@ -21,6 +21,9 @@ namespace ConnectionsAPI.Utility public string Editor { get; set; } = string.Empty; [JsonPropertyName("categories")] public IReadOnlyList Categories { get; set; } = []; + + [JsonIgnore] + public string Md5 { get; set; } = string.Empty; } class NYTConnectionsPuzzleCategory @@ -59,13 +62,26 @@ namespace ConnectionsAPI.Utility ConcurrentDictionary responses = new(); foreach (var batch in syncDates.Chunk(5)) { + ConcurrentBag batchPuzzles = []; await Task.WhenAll( batch.Select(x => GetConnectionsResponseAsync(x, ct).ContinueWith(t => { string? result = t.Result; if (!string.IsNullOrWhiteSpace(result)) { - responses.TryAdd(x, result); + try + { + var nytResponseJson = JsonSerializer.Deserialize(result) + ?? throw new InvalidDataException("Connections response deserialized to null"); + + string md5 = HashUtility.CalculateMD5(result); + nytResponseJson.Md5 = md5; + batchPuzzles.Add(nytResponseJson); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to deserialize Connections response for {date}", x); + } } else { @@ -73,59 +89,44 @@ namespace ConnectionsAPI.Utility } })) ); - } - // process the response data - foreach (var response in responses.Select(kvp => new { PrintDate = kvp.Key, JsonContent = kvp.Value }) - .OrderBy(x => x.PrintDate)) - { - _logger.LogInformation("Processing puzzle data for {printDate}", response.PrintDate); - await UpsertPuzzleDataAsync(response.PrintDate, response.JsonContent); + foreach (var puzzle in batchPuzzles.OrderBy(x => x.PrintDate)) + { + await UpsertPuzzleDataAsync(puzzle); + } + await _db.SaveChangesAsync(ct); } - - await _db.SaveChangesAsync(ct); } - private async Task UpsertPuzzleDataAsync(string printDate, string puzzleJson) + private async Task UpsertPuzzleDataAsync(NYTConnectionsPuzzle nytPuzzle) { - // check if JSON is valid - NYTConnectionsPuzzle? nytPuzzle = JsonSerializer.Deserialize(puzzleJson); - if (nytPuzzle == null || nytPuzzle.Status != "OK") - { - _logger.LogError("JSON content for {printDate} failed to deserialize or status not OK", printDate); - return; - } - - // calculate JSON content hash for change detection - string jsonMD5 = HashUtility.CalculateMD5(puzzleJson); - // get a tracking reference to the puzzle matching by print date, either by querying or creating a new entity - var puzzle = await _db.Puzzles + var puzzle = await _db.CategoriesPuzzles .Include(x => x.Categories) - .ThenInclude(x => x.PuzzleCards) - .FirstOrDefaultAsync(x => x.PrintDate == printDate); + .ThenInclude(x => x.CategoriesPuzzleCards) + .FirstOrDefaultAsync(x => x.PrintDate == nytPuzzle.PrintDate); if (puzzle == null) { - _logger.LogTrace("No puzzle found for {printDate}, puzzle will be created", printDate); - puzzle = new Database.Entities.Puzzle + _logger.LogTrace("No puzzle found for {printDate}, puzzle will be created", nytPuzzle.PrintDate); + puzzle = new Database.Entities.CategoriesPuzzle { Categories = [], CreatedDate = DateTime.UtcNow }; - _db.Puzzles.Add(puzzle); + _db.CategoriesPuzzles.Add(puzzle); } // if the content hash matches, no update needed - if (puzzle.ContentMD5 == jsonMD5) + if (puzzle.ContentMD5 == nytPuzzle.Md5) { - _logger.LogTrace("JSON content hash for {printDate} matches, no need for update", printDate); + _logger.LogTrace("JSON content hash for {printDate} matches, no need for update", nytPuzzle.PrintDate); return; } - puzzle.ContentMD5 = jsonMD5; - puzzle.PrintDate = printDate; + puzzle.ContentMD5 = nytPuzzle.Md5; + puzzle.PrintDate = nytPuzzle.PrintDate; puzzle.EditorName = nytPuzzle.Editor; - puzzle.Index = CalculateConnectionsDayIndex(printDate); + puzzle.Index = CalculateConnectionsDayIndex(nytPuzzle.PrintDate); puzzle.Categories ??= []; // mark items for deletion and also remove them from here to be readded @@ -136,23 +137,23 @@ namespace ConnectionsAPI.Utility int idx = 1; foreach (var nytCategory in nytPuzzle.Categories) { - PuzzleCategory category = new() + CategoriesCategory category = new() { - Color = (PuzzleCategoryColor)idx++, + Color = (CategoriesColor)idx++, Name = nytCategory.Title, - Puzzle = puzzle, - PuzzleCards = [] + CategoriesPuzzle = puzzle, + CategoriesPuzzleCards = [] }; foreach (var nytCard in nytCategory.Cards) { - PuzzleCard card = new() + CategoriesCard card = new() { Content = nytCard.Content, Position = nytCard.Position, - PuzzleCategory = category, + Category = category, }; - category.PuzzleCards.Add(card); + category.CategoriesPuzzleCards.Add(card); } puzzle.Categories.Add(category); @@ -181,7 +182,7 @@ namespace ConnectionsAPI.Utility private async Task> GetSyncDatesAsync(CancellationToken ct) { // query the last puzzle we have in the database - string? lastSyncedPuzzleDate = await _db.Puzzles.AsNoTracking() + string? lastSyncedPuzzleDate = await _db.CategoriesPuzzles.AsNoTracking() .OrderByDescending(x => x.PrintDate) .Select(x => x.PrintDate) .FirstOrDefaultAsync(cancellationToken: ct); diff --git a/appsettings.Development.json b/appsettings.Development.json index 8123359..74156a1 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -6,7 +6,7 @@ } }, "ConnectionStrings": { - "ConnectionsContext": "Data Source=c:\\tmp\\connections-api\\dev.db;" + "ConnectionsContext": "Data Source=.tmp/dev.db;" }, "Sync": { "RunImmediately": true