refactor: Refactor syncing Connections puzzles

This commit is contained in:
2024-12-26 13:35:41 +01:00
parent 051c124855
commit a1950b7586
12 changed files with 458 additions and 135 deletions

View File

@@ -5,15 +5,15 @@ namespace ConnectionsAPI.Database
{ {
public class ConnectionsContext(DbContextOptions<ConnectionsContext> dbContextOptions) : DbContext(dbContextOptions) public class ConnectionsContext(DbContextOptions<ConnectionsContext> dbContextOptions) : DbContext(dbContextOptions)
{ {
public DbSet<Puzzle> Puzzles { get; set; } public DbSet<CategoriesPuzzle> CategoriesPuzzles { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
modelBuilder.Entity<Puzzle>() modelBuilder.Entity<CategoriesPuzzle>()
.HasIndex(x => x.PrintDate).IsUnique(); .HasIndex(x => x.PrintDate).IsUnique();
modelBuilder.Entity<Puzzle>() modelBuilder.Entity<CategoriesPuzzle>()
.Ignore(x => x.NextPrintDate); .Ignore(x => x.NextPrintDate);
modelBuilder.Entity<Puzzle>() modelBuilder.Entity<CategoriesPuzzle>()
.Ignore(x => x.PrevPrintDate); .Ignore(x => x.PrevPrintDate);

View File

@@ -1,6 +1,6 @@
namespace ConnectionsAPI.Database.Entities namespace ConnectionsAPI.Database.Entities
{ {
public class PuzzleCard public class CategoriesCard
{ {
/// <summary> /// <summary>
/// Primary key of the entity /// Primary key of the entity
@@ -20,11 +20,11 @@
/// <summary> /// <summary>
/// The ID of the associated Connections category /// The ID of the associated Connections category
/// </summary> /// </summary>
public int PuzzleCategoryId { get; set; } public int CategoriesCategoryId { get; set; }
/// <summary> /// <summary>
/// The associated category instance /// The associated category instance
/// </summary> /// </summary>
public virtual PuzzleCategory? PuzzleCategory { get; set; } public virtual CategoriesCategory? Category { get; set; }
} }
} }

View File

@@ -1,6 +1,6 @@
namespace ConnectionsAPI.Database.Entities namespace ConnectionsAPI.Database.Entities
{ {
public class PuzzleCategory public class CategoriesCategory
{ {
/// <summary> /// <summary>
/// Primary key of the entity /// Primary key of the entity
@@ -15,21 +15,21 @@
/// <summary> /// <summary>
/// The color of the category in this Connections puzzle; Also used for sorting /// The color of the category in this Connections puzzle; Also used for sorting
/// </summary> /// </summary>
public PuzzleCategoryColor Color { get; set; } public CategoriesColor Color { get; set; }
/// <summary> /// <summary>
/// The ID of the associated Connections puzzle /// The ID of the associated Connections puzzle
/// </summary> /// </summary>
public int PuzzleId { get; set; } public int CategoriesPuzzleId { get; set; }
/// <summary> /// <summary>
/// The associated puzzle instance /// The associated puzzle instance
/// </summary> /// </summary>
public virtual Puzzle? Puzzle { get; set; } public virtual CategoriesPuzzle? CategoriesPuzzle { get; set; }
/// <summary> /// <summary>
/// The cards associated with this category /// The cards associated with this category
/// </summary> /// </summary>
public ICollection<PuzzleCard> PuzzleCards { get; set; } = []; public ICollection<CategoriesCard> CategoriesPuzzleCards { get; set; } = [];
} }
} }

View File

@@ -1,6 +1,6 @@
namespace ConnectionsAPI.Database.Entities namespace ConnectionsAPI.Database.Entities
{ {
public enum PuzzleCategoryColor public enum CategoriesColor
{ {
Yellow = 1, Yellow = 1,
Green = 2, Green = 2,

View File

@@ -2,7 +2,7 @@
namespace ConnectionsAPI.Database.Entities namespace ConnectionsAPI.Database.Entities
{ {
public class Puzzle public class CategoriesPuzzle
{ {
/// <summary> /// <summary>
/// Primary key of the entity /// Primary key of the entity
@@ -37,7 +37,7 @@ namespace ConnectionsAPI.Database.Entities
/// <summary> /// <summary>
/// The categories associated with this puzzle /// The categories associated with this puzzle
/// </summary> /// </summary>
public virtual ICollection<PuzzleCategory> Categories { get; set; } = []; public virtual ICollection<CategoriesCategory> Categories { get; set; } = [];
[NotMapped] [NotMapped]
public string? PrevPrintDate { get; set; } public string? PrevPrintDate { get; set; }

View File

@@ -0,0 +1,135 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CategoriesCategoryId")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Position")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CategoriesCategoryId");
b.ToTable("CategoriesCard");
});
modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CategoriesPuzzleId")
.HasColumnType("INTEGER");
b.Property<int>("Color")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CategoriesPuzzleId");
b.ToTable("CategoriesCategory");
});
modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesPuzzle", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ContentMD5")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedDate")
.HasColumnType("TEXT");
b.Property<string>("EditorName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<string>("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
}
}
}

View File

@@ -0,0 +1,187 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ConnectionsAPI.Database.Migrations
{
/// <inheritdoc />
public partial class RenameCategoriesPuzzles : Migration
{
/// <inheritdoc />
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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CreatedDate = table.Column<DateTime>(type: "TEXT", nullable: false),
PrintDate = table.Column<string>(type: "TEXT", nullable: false),
EditorName = table.Column<string>(type: "TEXT", nullable: false),
Index = table.Column<int>(type: "INTEGER", nullable: false),
ContentMD5 = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CategoriesPuzzles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "CategoriesCategory",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
Color = table.Column<int>(type: "INTEGER", nullable: false),
CategoriesPuzzleId = table.Column<int>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Content = table.Column<string>(type: "TEXT", nullable: false),
Position = table.Column<int>(type: "INTEGER", nullable: false),
CategoriesCategoryId = table.Column<int>(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);
}
/// <inheritdoc />
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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ContentMD5 = table.Column<string>(type: "TEXT", nullable: false),
CreatedDate = table.Column<DateTime>(type: "TEXT", nullable: false),
EditorName = table.Column<string>(type: "TEXT", nullable: false),
Index = table.Column<int>(type: "INTEGER", nullable: false),
PrintDate = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Puzzles", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PuzzleCategory",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
PuzzleId = table.Column<int>(type: "INTEGER", nullable: false),
Color = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(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<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
PuzzleCategoryId = table.Column<int>(type: "INTEGER", nullable: false),
Content = table.Column<string>(type: "TEXT", nullable: false),
Position = table.Column<int>(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);
}
}
}

View File

@@ -17,7 +17,53 @@ namespace ConnectionsAPI.Database.Migrations
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); modelBuilder.HasAnnotation("ProductVersion", "8.0.4");
modelBuilder.Entity("ConnectionsAPI.Database.Entities.Puzzle", b => modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCard", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CategoriesCategoryId")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Position")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CategoriesCategoryId");
b.ToTable("CategoriesCard");
});
modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CategoriesPuzzleId")
.HasColumnType("INTEGER");
b.Property<int>("Color")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CategoriesPuzzleId");
b.ToTable("CategoriesCategory");
});
modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesPuzzle", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -46,86 +92,40 @@ namespace ConnectionsAPI.Database.Migrations
b.HasIndex("PrintDate") b.HasIndex("PrintDate")
.IsUnique(); .IsUnique();
b.ToTable("Puzzles"); b.ToTable("CategoriesPuzzles");
}); });
modelBuilder.Entity("ConnectionsAPI.Database.Entities.PuzzleCard", b => modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCard", b =>
{ {
b.Property<int>("Id") b.HasOne("ConnectionsAPI.Database.Entities.CategoriesCategory", "Category")
.ValueGeneratedOnAdd() .WithMany("CategoriesPuzzleCards")
.HasColumnType("INTEGER"); .HasForeignKey("CategoriesCategoryId")
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Position")
.HasColumnType("INTEGER");
b.Property<int>("PuzzleCategoryId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PuzzleCategoryId");
b.ToTable("PuzzleCard");
});
modelBuilder.Entity("ConnectionsAPI.Database.Entities.PuzzleCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Color")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("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")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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") .WithMany("Categories")
.HasForeignKey("PuzzleId") .HasForeignKey("CategoriesPuzzleId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .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"); b.Navigation("Categories");
}); });
modelBuilder.Entity("ConnectionsAPI.Database.Entities.PuzzleCategory", b =>
{
b.Navigation("PuzzleCards");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@@ -7,16 +7,16 @@ namespace ConnectionsAPI.Database.Repository
{ {
private readonly ConnectionsContext _db = _db; private readonly ConnectionsContext _db = _db;
public async Task<Puzzle?> GetPuzzleByDateAsync(string printDate, bool includeSolutions = true) public async Task<CategoriesPuzzle?> GetPuzzleByDateAsync(string printDate, bool includeSolutions = true)
{ {
// query for the puzzle // query for the puzzle
var query = _db.Puzzles.AsNoTracking(); var query = _db.CategoriesPuzzles.AsNoTracking();
if (includeSolutions) if (includeSolutions)
{ {
query = query query = query
.Include(x => x.Categories) .Include(x => x.Categories)
.ThenInclude(x => x.PuzzleCards); .ThenInclude(x => x.CategoriesPuzzleCards);
} }
var puzzle = await query.FirstOrDefaultAsync(x => x.PrintDate == printDate); var puzzle = await query.FirstOrDefaultAsync(x => x.PrintDate == printDate);
@@ -32,17 +32,17 @@ namespace ConnectionsAPI.Database.Repository
return puzzle; return puzzle;
} }
public async Task<IEnumerable<Puzzle>> GetAllPuzzlesAsync(bool includeSolutions = true) public async Task<IEnumerable<CategoriesPuzzle>> GetAllPuzzlesAsync(bool includeSolutions = true)
{ {
// query all, ordered by print date // query all, ordered by print date
var query = _db.Puzzles var query = _db.CategoriesPuzzles
.AsNoTracking(); .AsNoTracking();
if (includeSolutions) if (includeSolutions)
{ {
query = query query = query
.Include(x => x.Categories) .Include(x => x.Categories)
.ThenInclude(x => x.PuzzleCards); .ThenInclude(x => x.CategoriesPuzzleCards);
} }
var result = (await query.OrderBy(x => x.PrintDate).ToListAsync()) ?? []; var result = (await query.OrderBy(x => x.PrintDate).ToListAsync()) ?? [];
@@ -55,15 +55,15 @@ namespace ConnectionsAPI.Database.Repository
return result; 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) .Where(x => x.PrintDate.CompareTo(puzzle.PrintDate) < 0)
.OrderByDescending(x => x.PrintDate) .OrderByDescending(x => x.PrintDate)
.Select(x => x.PrintDate) .Select(x => x.PrintDate)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
string? nextPuzzleDate = await _db.Puzzles.AsNoTracking() string? nextPuzzleDate = await _db.CategoriesPuzzles.AsNoTracking()
.Where(x => x.PrintDate.CompareTo(puzzle.PrintDate) > 0) .Where(x => x.PrintDate.CompareTo(puzzle.PrintDate) > 0)
.OrderBy(x => x.PrintDate) .OrderBy(x => x.PrintDate)
.Select(x => x.PrintDate) .Select(x => x.PrintDate)

View File

@@ -2,7 +2,7 @@
{ {
public class PuzzleDTO public class PuzzleDTO
{ {
public static PuzzleDTO FromEntity(Database.Entities.Puzzle dbPuzzle) => public static PuzzleDTO FromEntity(Database.Entities.CategoriesPuzzle dbPuzzle) =>
new() new()
{ {
PuzzleNumber = dbPuzzle.Index, PuzzleNumber = dbPuzzle.Index,
@@ -25,11 +25,11 @@
public class PuzzleCategoryDTO public class PuzzleCategoryDTO
{ {
public static PuzzleCategoryDTO FromEntity(Database.Entities.PuzzleCategory dbCategory) => public static PuzzleCategoryDTO FromEntity(Database.Entities.CategoriesCategory dbCategory) =>
new() new()
{ {
Title = dbCategory.Name, 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(), Color = dbCategory.Color.ToString().ToLower(),
OrderingKey = (int)dbCategory.Color OrderingKey = (int)dbCategory.Color
}; };
@@ -42,7 +42,7 @@
public class PuzzleCardDTO public class PuzzleCardDTO
{ {
public static PuzzleCardDTO FromEntity(Database.Entities.PuzzleCard dbCard) => public static PuzzleCardDTO FromEntity(Database.Entities.CategoriesCard dbCard) =>
new() new()
{ {
Content = dbCard.Content, Content = dbCard.Content,

View File

@@ -21,6 +21,9 @@ namespace ConnectionsAPI.Utility
public string Editor { get; set; } = string.Empty; public string Editor { get; set; } = string.Empty;
[JsonPropertyName("categories")] [JsonPropertyName("categories")]
public IReadOnlyList<NYTConnectionsPuzzleCategory> Categories { get; set; } = []; public IReadOnlyList<NYTConnectionsPuzzleCategory> Categories { get; set; } = [];
[JsonIgnore]
public string Md5 { get; set; } = string.Empty;
} }
class NYTConnectionsPuzzleCategory class NYTConnectionsPuzzleCategory
@@ -59,13 +62,26 @@ namespace ConnectionsAPI.Utility
ConcurrentDictionary<string, string> responses = new(); ConcurrentDictionary<string, string> responses = new();
foreach (var batch in syncDates.Chunk(5)) foreach (var batch in syncDates.Chunk(5))
{ {
ConcurrentBag<NYTConnectionsPuzzle> batchPuzzles = [];
await Task.WhenAll( await Task.WhenAll(
batch.Select(x => GetConnectionsResponseAsync(x, ct).ContinueWith(t => batch.Select(x => GetConnectionsResponseAsync(x, ct).ContinueWith(t =>
{ {
string? result = t.Result; string? result = t.Result;
if (!string.IsNullOrWhiteSpace(result)) if (!string.IsNullOrWhiteSpace(result))
{ {
responses.TryAdd(x, result); try
{
var nytResponseJson = JsonSerializer.Deserialize<NYTConnectionsPuzzle>(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 else
{ {
@@ -73,59 +89,44 @@ namespace ConnectionsAPI.Utility
} }
})) }))
); );
}
// process the response data foreach (var puzzle in batchPuzzles.OrderBy(x => x.PrintDate))
foreach (var response in responses.Select(kvp => new { PrintDate = kvp.Key, JsonContent = kvp.Value }) {
.OrderBy(x => x.PrintDate)) await UpsertPuzzleDataAsync(puzzle);
{ }
_logger.LogInformation("Processing puzzle data for {printDate}", response.PrintDate); await _db.SaveChangesAsync(ct);
await UpsertPuzzleDataAsync(response.PrintDate, response.JsonContent);
} }
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<NYTConnectionsPuzzle>(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 // 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) .Include(x => x.Categories)
.ThenInclude(x => x.PuzzleCards) .ThenInclude(x => x.CategoriesPuzzleCards)
.FirstOrDefaultAsync(x => x.PrintDate == printDate); .FirstOrDefaultAsync(x => x.PrintDate == nytPuzzle.PrintDate);
if (puzzle == null) if (puzzle == null)
{ {
_logger.LogTrace("No puzzle found for {printDate}, puzzle will be created", printDate); _logger.LogTrace("No puzzle found for {printDate}, puzzle will be created", nytPuzzle.PrintDate);
puzzle = new Database.Entities.Puzzle puzzle = new Database.Entities.CategoriesPuzzle
{ {
Categories = [], Categories = [],
CreatedDate = DateTime.UtcNow CreatedDate = DateTime.UtcNow
}; };
_db.Puzzles.Add(puzzle); _db.CategoriesPuzzles.Add(puzzle);
} }
// if the content hash matches, no update needed // 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; return;
} }
puzzle.ContentMD5 = jsonMD5; puzzle.ContentMD5 = nytPuzzle.Md5;
puzzle.PrintDate = printDate; puzzle.PrintDate = nytPuzzle.PrintDate;
puzzle.EditorName = nytPuzzle.Editor; puzzle.EditorName = nytPuzzle.Editor;
puzzle.Index = CalculateConnectionsDayIndex(printDate); puzzle.Index = CalculateConnectionsDayIndex(nytPuzzle.PrintDate);
puzzle.Categories ??= []; puzzle.Categories ??= [];
// mark items for deletion and also remove them from here to be readded // mark items for deletion and also remove them from here to be readded
@@ -136,23 +137,23 @@ namespace ConnectionsAPI.Utility
int idx = 1; int idx = 1;
foreach (var nytCategory in nytPuzzle.Categories) foreach (var nytCategory in nytPuzzle.Categories)
{ {
PuzzleCategory category = new() CategoriesCategory category = new()
{ {
Color = (PuzzleCategoryColor)idx++, Color = (CategoriesColor)idx++,
Name = nytCategory.Title, Name = nytCategory.Title,
Puzzle = puzzle, CategoriesPuzzle = puzzle,
PuzzleCards = [] CategoriesPuzzleCards = []
}; };
foreach (var nytCard in nytCategory.Cards) foreach (var nytCard in nytCategory.Cards)
{ {
PuzzleCard card = new() CategoriesCard card = new()
{ {
Content = nytCard.Content, Content = nytCard.Content,
Position = nytCard.Position, Position = nytCard.Position,
PuzzleCategory = category, Category = category,
}; };
category.PuzzleCards.Add(card); category.CategoriesPuzzleCards.Add(card);
} }
puzzle.Categories.Add(category); puzzle.Categories.Add(category);
@@ -181,7 +182,7 @@ namespace ConnectionsAPI.Utility
private async Task<IReadOnlyList<string>> GetSyncDatesAsync(CancellationToken ct) private async Task<IReadOnlyList<string>> GetSyncDatesAsync(CancellationToken ct)
{ {
// query the last puzzle we have in the database // 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) .OrderByDescending(x => x.PrintDate)
.Select(x => x.PrintDate) .Select(x => x.PrintDate)
.FirstOrDefaultAsync(cancellationToken: ct); .FirstOrDefaultAsync(cancellationToken: ct);

View File

@@ -6,7 +6,7 @@
} }
}, },
"ConnectionStrings": { "ConnectionStrings": {
"ConnectionsContext": "Data Source=c:\\tmp\\connections-api\\dev.db;" "ConnectionsContext": "Data Source=.tmp/dev.db;"
}, },
"Sync": { "Sync": {
"RunImmediately": true "RunImmediately": true