refactor: properly rename connections-related tables; top-level namespaces
This commit is contained in:
@@ -1,23 +1,22 @@
|
|||||||
using ConnectionsAPI.Database.Entities;
|
using ConnectionsAPI.Database.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ConnectionsAPI.Database
|
namespace ConnectionsAPI.Database;
|
||||||
|
public class ConnectionsContext(DbContextOptions<ConnectionsContext> dbContextOptions) : DbContext(dbContextOptions)
|
||||||
{
|
{
|
||||||
public class ConnectionsContext(DbContextOptions<ConnectionsContext> dbContextOptions) : DbContext(dbContextOptions)
|
public required DbSet<ConnectionsPuzzle> ConnectionsPuzzles { get; set; }
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
public DbSet<CategoriesPuzzle> CategoriesPuzzles { get; set; }
|
modelBuilder.Entity<ConnectionsPuzzle>()
|
||||||
|
.HasIndex(x => x.PrintDate).IsUnique();
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
modelBuilder.Entity<ConnectionsPuzzle>()
|
||||||
{
|
.Ignore(x => x.NextPrintDate);
|
||||||
modelBuilder.Entity<CategoriesPuzzle>()
|
modelBuilder.Entity<ConnectionsPuzzle>()
|
||||||
.HasIndex(x => x.PrintDate).IsUnique();
|
.Ignore(x => x.PrevPrintDate);
|
||||||
modelBuilder.Entity<CategoriesPuzzle>()
|
|
||||||
.Ignore(x => x.NextPrintDate);
|
|
||||||
modelBuilder.Entity<CategoriesPuzzle>()
|
|
||||||
.Ignore(x => x.PrevPrintDate);
|
|
||||||
|
|
||||||
|
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
namespace ConnectionsAPI.Database.Entities
|
|
||||||
{
|
|
||||||
public class CategoriesCard
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Primary key of the entity
|
|
||||||
/// </summary>
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The contents of this card (the word)
|
|
||||||
/// </summary>
|
|
||||||
public string Content { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The initial position of this card on the grid
|
|
||||||
/// </summary>
|
|
||||||
public int Position { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the associated Connections category
|
|
||||||
/// </summary>
|
|
||||||
public int CategoriesCategoryId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The associated category instance
|
|
||||||
/// </summary>
|
|
||||||
public virtual CategoriesCategory? Category { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
namespace ConnectionsAPI.Database.Entities
|
|
||||||
{
|
|
||||||
public class CategoriesCategory
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Primary key of the entity
|
|
||||||
/// </summary>
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the category in this Connections puzzle
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The color of the category in this Connections puzzle; Also used for sorting
|
|
||||||
/// </summary>
|
|
||||||
public CategoriesColor Color { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the associated Connections puzzle
|
|
||||||
/// </summary>
|
|
||||||
public int CategoriesPuzzleId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The associated puzzle instance
|
|
||||||
/// </summary>
|
|
||||||
public virtual CategoriesPuzzle? CategoriesPuzzle { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The cards associated with this category
|
|
||||||
/// </summary>
|
|
||||||
public ICollection<CategoriesCard> CategoriesPuzzleCards { get; set; } = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace ConnectionsAPI.Database.Entities
|
|
||||||
{
|
|
||||||
public enum CategoriesColor
|
|
||||||
{
|
|
||||||
Yellow = 1,
|
|
||||||
Green = 2,
|
|
||||||
Blue = 3,
|
|
||||||
Purple = 4,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace ConnectionsAPI.Database.Entities
|
|
||||||
{
|
|
||||||
public class CategoriesPuzzle
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Primary key of the entity
|
|
||||||
/// </summary>
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When the entity was created (is the sync date)
|
|
||||||
/// </summary>
|
|
||||||
public DateTime CreatedDate { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When the puzzle was "printed" online
|
|
||||||
/// </summary>
|
|
||||||
public string PrintDate { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the editor for the puzzle
|
|
||||||
/// </summary>
|
|
||||||
public string EditorName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The actual count of the puzzle
|
|
||||||
/// </summary>
|
|
||||||
public int Index { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The MD5 hash for the source content used to sync this puzzle
|
|
||||||
/// </summary>
|
|
||||||
public string ContentMD5 { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The categories associated with this puzzle
|
|
||||||
/// </summary>
|
|
||||||
public virtual ICollection<CategoriesCategory> Categories { get; set; } = [];
|
|
||||||
|
|
||||||
[NotMapped]
|
|
||||||
public string? PrevPrintDate { get; set; }
|
|
||||||
[NotMapped]
|
|
||||||
public string? NextPrintDate { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
Database/Entities/ConnectionsCard.cs
Normal file
29
Database/Entities/ConnectionsCard.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
namespace ConnectionsAPI.Database.Entities;
|
||||||
|
|
||||||
|
public class ConnectionsCard
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Primary key of the entity
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The contents of this card (the word)
|
||||||
|
/// </summary>
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The initial position of this card on the grid
|
||||||
|
/// </summary>
|
||||||
|
public int Position { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the associated Connections category
|
||||||
|
/// </summary>
|
||||||
|
public int ConnectionsCategoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The associated category instance
|
||||||
|
/// </summary>
|
||||||
|
public virtual ConnectionsCategory? Category { get; set; }
|
||||||
|
}
|
||||||
34
Database/Entities/ConnectionsCategory.cs
Normal file
34
Database/Entities/ConnectionsCategory.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
namespace ConnectionsAPI.Database.Entities;
|
||||||
|
|
||||||
|
public class ConnectionsCategory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Primary key of the entity
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the category in this Connections puzzle
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The color of the category in this Connections puzzle; Also used for sorting
|
||||||
|
/// </summary>
|
||||||
|
public ConnectionsColor Color { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the associated Connections puzzle
|
||||||
|
/// </summary>
|
||||||
|
public int ConnectionsPuzzleId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The associated puzzle instance
|
||||||
|
/// </summary>
|
||||||
|
public virtual ConnectionsPuzzle? ConnectionsPuzzle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The cards associated with this category
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<ConnectionsCard> Cards { get; set; } = [];
|
||||||
|
}
|
||||||
10
Database/Entities/ConnectionsColor.cs
Normal file
10
Database/Entities/ConnectionsColor.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace ConnectionsAPI.Database.Entities;
|
||||||
|
|
||||||
|
public enum ConnectionsColor
|
||||||
|
{
|
||||||
|
Yellow = 1,
|
||||||
|
Green = 2,
|
||||||
|
Blue = 3,
|
||||||
|
Purple = 4,
|
||||||
|
}
|
||||||
|
|
||||||
45
Database/Entities/ConnectionsPuzzle.cs
Normal file
45
Database/Entities/ConnectionsPuzzle.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace ConnectionsAPI.Database.Entities;
|
||||||
|
public class ConnectionsPuzzle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Primary key of the entity
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the entity was created (is the sync date)
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the puzzle was "printed" online
|
||||||
|
/// </summary>
|
||||||
|
public string PrintDate { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the editor for the puzzle
|
||||||
|
/// </summary>
|
||||||
|
public string EditorName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The actual count of the puzzle
|
||||||
|
/// </summary>
|
||||||
|
public int Index { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The MD5 hash for the source content used to sync this puzzle
|
||||||
|
/// </summary>
|
||||||
|
public string ContentMD5 { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The categories associated with this puzzle
|
||||||
|
/// </summary>
|
||||||
|
public virtual ICollection<ConnectionsCategory> Categories { get; set; } = [];
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
public string? PrevPrintDate { get; set; }
|
||||||
|
[NotMapped]
|
||||||
|
public string? NextPrintDate { get; set; }
|
||||||
|
}
|
||||||
136
Database/Migrations/20241226124356_RenameTablesCorrectly.Designer.cs
generated
Normal file
136
Database/Migrations/20241226124356_RenameTablesCorrectly.Designer.cs
generated
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// <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("20241226124356_RenameTablesCorrectly")]
|
||||||
|
partial class RenameTablesCorrectly
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.4");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCard", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("CategoriesCategoryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("CategoryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CategoryId");
|
||||||
|
|
||||||
|
b.ToTable("ConnectionsCard");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Color")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ConnectionsPuzzleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ConnectionsPuzzleId");
|
||||||
|
|
||||||
|
b.ToTable("ConnectionsCategory");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", 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.ConnectionsCard", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ConnectionsAPI.Database.Entities.ConnectionsCategory", "Category")
|
||||||
|
.WithMany("Cards")
|
||||||
|
.HasForeignKey("CategoryId");
|
||||||
|
|
||||||
|
b.Navigation("Category");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", "ConnectionsPuzzle")
|
||||||
|
.WithMany("Categories")
|
||||||
|
.HasForeignKey("ConnectionsPuzzleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("ConnectionsPuzzle");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Cards");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Categories");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
Database/Migrations/20241226124356_RenameTablesCorrectly.cs
Normal file
134
Database/Migrations/20241226124356_RenameTablesCorrectly.cs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ConnectionsAPI.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RenameTablesCorrectly : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CategoriesCard");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "CategoriesCategory");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ConnectionsCategory",
|
||||||
|
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),
|
||||||
|
ConnectionsPuzzleId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ConnectionsCategory", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ConnectionsCategory_CategoriesPuzzles_ConnectionsPuzzleId",
|
||||||
|
column: x => x.ConnectionsPuzzleId,
|
||||||
|
principalTable: "CategoriesPuzzles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ConnectionsCard",
|
||||||
|
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),
|
||||||
|
CategoryId = table.Column<int>(type: "INTEGER", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ConnectionsCard", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ConnectionsCard_ConnectionsCategory_CategoryId",
|
||||||
|
column: x => x.CategoryId,
|
||||||
|
principalTable: "ConnectionsCategory",
|
||||||
|
principalColumn: "Id");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ConnectionsCard_CategoryId",
|
||||||
|
table: "ConnectionsCard",
|
||||||
|
column: "CategoryId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ConnectionsCategory_ConnectionsPuzzleId",
|
||||||
|
table: "ConnectionsCategory",
|
||||||
|
column: "ConnectionsPuzzleId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ConnectionsCard");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ConnectionsCategory");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "CategoriesCategory",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
CategoriesPuzzleId = 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_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),
|
||||||
|
CategoriesCategoryId = 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_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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
Database/Migrations/20241226124529_RenameCollectionsTable.Designer.cs
generated
Normal file
136
Database/Migrations/20241226124529_RenameCollectionsTable.Designer.cs
generated
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// <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("20241226124529_RenameCollectionsTable")]
|
||||||
|
partial class RenameCollectionsTable
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.4");
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCard", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("CategoriesCategoryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("CategoryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CategoryId");
|
||||||
|
|
||||||
|
b.ToTable("ConnectionsCard");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Color")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("ConnectionsPuzzleId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ConnectionsPuzzleId");
|
||||||
|
|
||||||
|
b.ToTable("ConnectionsCategory");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", 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("ConnectionsPuzzles");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCard", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ConnectionsAPI.Database.Entities.ConnectionsCategory", "Category")
|
||||||
|
.WithMany("Cards")
|
||||||
|
.HasForeignKey("CategoryId");
|
||||||
|
|
||||||
|
b.Navigation("Category");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", "ConnectionsPuzzle")
|
||||||
|
.WithMany("Categories")
|
||||||
|
.HasForeignKey("ConnectionsPuzzleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("ConnectionsPuzzle");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Cards");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Categories");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
Database/Migrations/20241226124529_RenameCollectionsTable.cs
Normal file
78
Database/Migrations/20241226124529_RenameCollectionsTable.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ConnectionsAPI.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RenameCollectionsTable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ConnectionsCategory_CategoriesPuzzles_ConnectionsPuzzleId",
|
||||||
|
table: "ConnectionsCategory");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_CategoriesPuzzles",
|
||||||
|
table: "CategoriesPuzzles");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "CategoriesPuzzles",
|
||||||
|
newName: "ConnectionsPuzzles");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_CategoriesPuzzles_PrintDate",
|
||||||
|
table: "ConnectionsPuzzles",
|
||||||
|
newName: "IX_ConnectionsPuzzles_PrintDate");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_ConnectionsPuzzles",
|
||||||
|
table: "ConnectionsPuzzles",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ConnectionsCategory_ConnectionsPuzzles_ConnectionsPuzzleId",
|
||||||
|
table: "ConnectionsCategory",
|
||||||
|
column: "ConnectionsPuzzleId",
|
||||||
|
principalTable: "ConnectionsPuzzles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ConnectionsCategory_ConnectionsPuzzles_ConnectionsPuzzleId",
|
||||||
|
table: "ConnectionsCategory");
|
||||||
|
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_ConnectionsPuzzles",
|
||||||
|
table: "ConnectionsPuzzles");
|
||||||
|
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "ConnectionsPuzzles",
|
||||||
|
newName: "CategoriesPuzzles");
|
||||||
|
|
||||||
|
migrationBuilder.RenameIndex(
|
||||||
|
name: "IX_ConnectionsPuzzles_PrintDate",
|
||||||
|
table: "CategoriesPuzzles",
|
||||||
|
newName: "IX_CategoriesPuzzles_PrintDate");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_CategoriesPuzzles",
|
||||||
|
table: "CategoriesPuzzles",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ConnectionsCategory_CategoriesPuzzles_ConnectionsPuzzleId",
|
||||||
|
table: "ConnectionsCategory",
|
||||||
|
column: "ConnectionsPuzzleId",
|
||||||
|
principalTable: "CategoriesPuzzles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ 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.CategoriesCard", b =>
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCard", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -26,6 +26,9 @@ namespace ConnectionsAPI.Database.Migrations
|
|||||||
b.Property<int>("CategoriesCategoryId")
|
b.Property<int>("CategoriesCategoryId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("CategoryId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Content")
|
b.Property<string>("Content")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@@ -35,21 +38,21 @@ namespace ConnectionsAPI.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("CategoriesCategoryId");
|
b.HasIndex("CategoryId");
|
||||||
|
|
||||||
b.ToTable("CategoriesCard");
|
b.ToTable("ConnectionsCard");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b =>
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("CategoriesPuzzleId")
|
b.Property<int>("Color")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("Color")
|
b.Property<int>("ConnectionsPuzzleId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
@@ -58,12 +61,12 @@ namespace ConnectionsAPI.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("CategoriesPuzzleId");
|
b.HasIndex("ConnectionsPuzzleId");
|
||||||
|
|
||||||
b.ToTable("CategoriesCategory");
|
b.ToTable("ConnectionsCategory");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesPuzzle", b =>
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -92,37 +95,35 @@ namespace ConnectionsAPI.Database.Migrations
|
|||||||
b.HasIndex("PrintDate")
|
b.HasIndex("PrintDate")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("CategoriesPuzzles");
|
b.ToTable("ConnectionsPuzzles");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCard", b =>
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCard", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("ConnectionsAPI.Database.Entities.CategoriesCategory", "Category")
|
b.HasOne("ConnectionsAPI.Database.Entities.ConnectionsCategory", "Category")
|
||||||
.WithMany("CategoriesPuzzleCards")
|
.WithMany("Cards")
|
||||||
.HasForeignKey("CategoriesCategoryId")
|
.HasForeignKey("CategoryId");
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Category");
|
b.Navigation("Category");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b =>
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("ConnectionsAPI.Database.Entities.CategoriesPuzzle", "CategoriesPuzzle")
|
b.HasOne("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", "ConnectionsPuzzle")
|
||||||
.WithMany("Categories")
|
.WithMany("Categories")
|
||||||
.HasForeignKey("CategoriesPuzzleId")
|
.HasForeignKey("ConnectionsPuzzleId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.Navigation("CategoriesPuzzle");
|
b.Navigation("ConnectionsPuzzle");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesCategory", b =>
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsCategory", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("CategoriesPuzzleCards");
|
b.Navigation("Cards");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("ConnectionsAPI.Database.Entities.CategoriesPuzzle", b =>
|
modelBuilder.Entity("ConnectionsAPI.Database.Entities.ConnectionsPuzzle", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Categories");
|
b.Navigation("Categories");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,76 +1,75 @@
|
|||||||
using ConnectionsAPI.Database.Entities;
|
using ConnectionsAPI.Database.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ConnectionsAPI.Database.Repository
|
namespace ConnectionsAPI.Database.Repository;
|
||||||
|
|
||||||
|
public class PuzzleRepository(ConnectionsContext _db)
|
||||||
{
|
{
|
||||||
public class PuzzleRepository(ConnectionsContext _db)
|
private readonly ConnectionsContext _db = _db;
|
||||||
|
|
||||||
|
public async Task<ConnectionsPuzzle?> GetPuzzleByDateAsync(string printDate, bool includeSolutions = true)
|
||||||
{
|
{
|
||||||
private readonly ConnectionsContext _db = _db;
|
// query for the puzzle
|
||||||
|
var query = _db.ConnectionsPuzzles.AsNoTracking();
|
||||||
|
|
||||||
public async Task<CategoriesPuzzle?> GetPuzzleByDateAsync(string printDate, bool includeSolutions = true)
|
if (includeSolutions)
|
||||||
{
|
{
|
||||||
// query for the puzzle
|
query = query
|
||||||
var query = _db.CategoriesPuzzles.AsNoTracking();
|
.Include(x => x.Categories)
|
||||||
|
.ThenInclude(x => x.Cards);
|
||||||
|
}
|
||||||
|
|
||||||
if (includeSolutions)
|
var puzzle = await query.FirstOrDefaultAsync(x => x.PrintDate == printDate);
|
||||||
{
|
|
||||||
query = query
|
|
||||||
.Include(x => x.Categories)
|
|
||||||
.ThenInclude(x => x.CategoriesPuzzleCards);
|
|
||||||
}
|
|
||||||
|
|
||||||
var puzzle = await query.FirstOrDefaultAsync(x => x.PrintDate == printDate);
|
// if not found, we're done here
|
||||||
|
if (puzzle == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// if not found, we're done here
|
await EnhancePuzzleWithDatesAsync(puzzle);
|
||||||
if (puzzle == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return puzzle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ConnectionsPuzzle>> GetAllPuzzlesAsync(bool includeSolutions = true)
|
||||||
|
{
|
||||||
|
// query all, ordered by print date
|
||||||
|
var query = _db.ConnectionsPuzzles
|
||||||
|
.AsNoTracking();
|
||||||
|
|
||||||
|
if (includeSolutions)
|
||||||
|
{
|
||||||
|
query = query
|
||||||
|
.Include(x => x.Categories)
|
||||||
|
.ThenInclude(x => x.Cards);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = (await query.OrderBy(x => x.PrintDate).ToListAsync()) ?? [];
|
||||||
|
|
||||||
|
foreach (var puzzle in result)
|
||||||
|
{
|
||||||
await EnhancePuzzleWithDatesAsync(puzzle);
|
await EnhancePuzzleWithDatesAsync(puzzle);
|
||||||
|
|
||||||
return puzzle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<CategoriesPuzzle>> GetAllPuzzlesAsync(bool includeSolutions = true)
|
return result;
|
||||||
{
|
}
|
||||||
// query all, ordered by print date
|
|
||||||
var query = _db.CategoriesPuzzles
|
|
||||||
.AsNoTracking();
|
|
||||||
|
|
||||||
if (includeSolutions)
|
private async Task EnhancePuzzleWithDatesAsync(ConnectionsPuzzle puzzle)
|
||||||
{
|
{
|
||||||
query = query
|
string? previousPuzzleDate = await _db.ConnectionsPuzzles.AsNoTracking()
|
||||||
.Include(x => x.Categories)
|
.Where(x => x.PrintDate.CompareTo(puzzle.PrintDate) < 0)
|
||||||
.ThenInclude(x => x.CategoriesPuzzleCards);
|
.OrderByDescending(x => x.PrintDate)
|
||||||
}
|
.Select(x => x.PrintDate)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
var result = (await query.OrderBy(x => x.PrintDate).ToListAsync()) ?? [];
|
string? nextPuzzleDate = await _db.ConnectionsPuzzles.AsNoTracking()
|
||||||
|
.Where(x => x.PrintDate.CompareTo(puzzle.PrintDate) > 0)
|
||||||
|
.OrderBy(x => x.PrintDate)
|
||||||
|
.Select(x => x.PrintDate)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
foreach (var puzzle in result)
|
puzzle.PrevPrintDate = previousPuzzleDate;
|
||||||
{
|
puzzle.NextPrintDate = nextPuzzleDate;
|
||||||
await EnhancePuzzleWithDatesAsync(puzzle);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task EnhancePuzzleWithDatesAsync(CategoriesPuzzle puzzle)
|
|
||||||
{
|
|
||||||
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.CategoriesPuzzles.AsNoTracking()
|
|
||||||
.Where(x => x.PrintDate.CompareTo(puzzle.PrintDate) > 0)
|
|
||||||
.OrderBy(x => x.PrintDate)
|
|
||||||
.Select(x => x.PrintDate)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
puzzle.PrevPrintDate = previousPuzzleDate;
|
|
||||||
puzzle.NextPrintDate = nextPuzzleDate;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,39 +3,40 @@ using ConnectionsAPI.Database;
|
|||||||
using ConnectionsAPI.Utility;
|
using ConnectionsAPI.Utility;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace ConnectionsAPI.Events
|
namespace ConnectionsAPI.Events;
|
||||||
|
|
||||||
|
public class ConnectionsSyncEvent : IEvent { }
|
||||||
|
|
||||||
|
public class ConnectionsSyncHandler(ILogger<ConnectionsSyncHandler> logger,
|
||||||
|
IServiceScopeFactory scopeFactory) : IEventHandler<ConnectionsSyncEvent>
|
||||||
{
|
{
|
||||||
public class PuzzleSyncEvent : IEvent { }
|
private readonly ILogger<ConnectionsSyncHandler> _logger = logger;
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory = scopeFactory;
|
||||||
|
|
||||||
public class PuzzleSyncHandler(ILogger<PuzzleSyncHandler> logger, IServiceScopeFactory scopeFactory) : IEventHandler<PuzzleSyncEvent>
|
public async Task HandleAsync(ConnectionsSyncEvent eventModel, CancellationToken ct)
|
||||||
{
|
{
|
||||||
private readonly ILogger<PuzzleSyncHandler> _logger = logger;
|
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||||
private readonly IServiceScopeFactory _scopeFactory = scopeFactory;
|
_logger.LogInformation("Received Connections Sync Event");
|
||||||
|
try
|
||||||
public async Task HandleAsync(PuzzleSyncEvent eventModel, CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
// construct scope
|
||||||
_logger.LogInformation("Received Puzzle Sync Event");
|
using var scope = _scopeFactory.CreateScope();
|
||||||
try
|
// get dependencies
|
||||||
{
|
ConnectionsContext db = scope.ServiceProvider.GetRequiredService<ConnectionsContext>();
|
||||||
// construct scope
|
HttpClient http = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>().CreateClient();
|
||||||
using var scope = _scopeFactory.CreateScope();
|
ILogger<SyncUtility> syncLogger = scope.ServiceProvider.GetRequiredService<ILogger<SyncUtility>>();
|
||||||
// get dependencies
|
// do the work
|
||||||
ConnectionsContext db = scope.ServiceProvider.GetRequiredService<ConnectionsContext>();
|
await new SyncUtility(db, syncLogger, http).SyncPuzzlesAsync(ct);
|
||||||
HttpClient http = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>().CreateClient();
|
|
||||||
ILogger<SyncUtility> syncLogger = scope.ServiceProvider.GetRequiredService<ILogger<SyncUtility>>();
|
|
||||||
// do the work
|
|
||||||
await new SyncUtility(db, syncLogger, http).SyncPuzzlesAsync(ct);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error while executing puzzle sync event");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
stopwatch.Stop();
|
|
||||||
}
|
|
||||||
_logger.LogInformation("Puzzle Sync Event finished in {ts}", stopwatch.Elapsed);
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error while executing Connections sync event");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
}
|
||||||
|
_logger.LogInformation("Connections Sync Event finished in {ts}", stopwatch.Elapsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace ConnectionsAPI.Features.Puzzle.Get
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GetPuzzleEndpoint(PuzzleRepository puzzleRepo, ILogger<GetPuzzleEndpoint> logger, IAppCache cache) : Endpoint<GetPuzzleEndpointRequest, PuzzleDTO>
|
public class GetPuzzleEndpoint(PuzzleRepository puzzleRepo, ILogger<GetPuzzleEndpoint> logger, IAppCache cache) : Endpoint<GetPuzzleEndpointRequest, ConnectionsPuzzleDTO>
|
||||||
{
|
{
|
||||||
private readonly PuzzleRepository _puzzleRepo = puzzleRepo;
|
private readonly PuzzleRepository _puzzleRepo = puzzleRepo;
|
||||||
private readonly ILogger<GetPuzzleEndpoint> _logger = logger;
|
private readonly ILogger<GetPuzzleEndpoint> _logger = logger;
|
||||||
@@ -58,7 +58,7 @@ namespace ConnectionsAPI.Features.Puzzle.Get
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get response from cache
|
// get response from cache
|
||||||
var response = PuzzleDTO.FromEntity(puzzle);
|
var response = ConnectionsPuzzleDTO.FromEntity(puzzle);
|
||||||
|
|
||||||
// done
|
// done
|
||||||
await SendAsync(response, cancellation: ct);
|
await SendAsync(response, cancellation: ct);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace ConnectionsAPI.Features.Puzzle.List
|
namespace ConnectionsAPI.Features.Puzzle.List
|
||||||
{
|
{
|
||||||
public class ListPuzzlesEndpoint(PuzzleRepository puzzleRepo, IAppCache cache) : EndpointWithoutRequest<ICollection<PuzzleDTO>>
|
public class ListPuzzlesEndpoint(PuzzleRepository puzzleRepo, IAppCache cache) : EndpointWithoutRequest<ICollection<ConnectionsPuzzleDTO>>
|
||||||
{
|
{
|
||||||
private readonly PuzzleRepository _puzzleRepo = puzzleRepo;
|
private readonly PuzzleRepository _puzzleRepo = puzzleRepo;
|
||||||
private readonly IAppCache _cache = cache;
|
private readonly IAppCache _cache = cache;
|
||||||
@@ -25,7 +25,7 @@ namespace ConnectionsAPI.Features.Puzzle.List
|
|||||||
var puzzles = await _puzzleRepo.GetAllPuzzlesAsync(includeSolutions: !hideSolutions);
|
var puzzles = await _puzzleRepo.GetAllPuzzlesAsync(includeSolutions: !hideSolutions);
|
||||||
|
|
||||||
// map to response object
|
// map to response object
|
||||||
var response = puzzles.Select(PuzzleDTO.FromEntity).ToList();
|
var response = puzzles.Select(ConnectionsPuzzleDTO.FromEntity).ToList();
|
||||||
|
|
||||||
// done
|
// done
|
||||||
await SendAsync(response, cancellation: ct);
|
await SendAsync(response, cancellation: ct);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace ConnectionsAPI.Models
|
namespace ConnectionsAPI.Models
|
||||||
{
|
{
|
||||||
public class PuzzleDTO
|
public class ConnectionsPuzzleDTO
|
||||||
{
|
{
|
||||||
public static PuzzleDTO FromEntity(Database.Entities.CategoriesPuzzle dbPuzzle) =>
|
public static ConnectionsPuzzleDTO FromEntity(Database.Entities.ConnectionsPuzzle 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.CategoriesCategory dbCategory) =>
|
public static PuzzleCategoryDTO FromEntity(Database.Entities.ConnectionsCategory dbCategory) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Title = dbCategory.Name,
|
Title = dbCategory.Name,
|
||||||
Cards = dbCategory.CategoriesPuzzleCards.OrderBy(x => x.Content).Select(PuzzleCardDTO.FromEntity).ToList(),
|
Cards = dbCategory.Cards.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.CategoriesCard dbCard) =>
|
public static PuzzleCardDTO FromEntity(Database.Entities.ConnectionsCard dbCard) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Content = dbCard.Content,
|
Content = dbCard.Content,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ namespace ConnectionsAPI
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Task SendSyncEvent(CancellationToken stoppingToken, bool wait = false) =>
|
private static Task SendSyncEvent(CancellationToken stoppingToken, bool wait = false) =>
|
||||||
new PuzzleSyncEvent { }.PublishAsync(wait ? Mode.WaitForAll : Mode.WaitForNone, stoppingToken);
|
new ConnectionsSyncEvent { }.PublishAsync(wait ? Mode.WaitForAll : Mode.WaitForNone, stoppingToken);
|
||||||
|
|
||||||
private async Task WaitForNextSchedule(CronExpression cron, CancellationToken ct)
|
private async Task WaitForNextSchedule(CronExpression cron, CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
namespace ConnectionsAPI.Utility
|
namespace ConnectionsAPI.Utility;
|
||||||
|
public static class EnvironmentUtility
|
||||||
{
|
{
|
||||||
public static class EnvironmentUtility
|
public static bool IsContainer =>
|
||||||
{
|
Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";
|
||||||
public static bool IsContainer =>
|
|
||||||
Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace ConnectionsAPI.Utility
|
namespace ConnectionsAPI.Utility;
|
||||||
|
public static class HashUtility
|
||||||
{
|
{
|
||||||
public static class HashUtility
|
public static string CalculateMD5(string input)
|
||||||
{
|
{
|
||||||
public static string CalculateMD5(string input)
|
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(input));
|
||||||
{
|
return Convert.ToHexString(hash).Replace("-", string.Empty).ToLower();
|
||||||
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(input));
|
|
||||||
return Convert.ToHexString(hash).Replace("-", string.Empty).ToLower();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,238 +6,236 @@ using System.Globalization;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ConnectionsAPI.Utility
|
namespace ConnectionsAPI.Utility;
|
||||||
|
public class SyncUtility(ConnectionsContext db, ILogger<SyncUtility> logger, HttpClient http)
|
||||||
{
|
{
|
||||||
public class SyncUtility(ConnectionsContext db, ILogger<SyncUtility> logger, HttpClient http)
|
#region Response types
|
||||||
|
class NYTConnectionsPuzzle
|
||||||
{
|
{
|
||||||
#region Response types
|
[JsonPropertyName("status")]
|
||||||
class NYTConnectionsPuzzle
|
public string Status { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("print_date")]
|
||||||
|
public string PrintDate { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("editor")]
|
||||||
|
public string Editor { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("categories")]
|
||||||
|
public IReadOnlyList<NYTConnectionsPuzzleCategory> Categories { get; set; } = [];
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string Md5 { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
class NYTConnectionsPuzzleCategory
|
||||||
|
{
|
||||||
|
[JsonPropertyName("title")]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("cards")]
|
||||||
|
public IReadOnlyList<NYTConnectionsPuzzleCard> Cards { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NYTConnectionsPuzzleCard
|
||||||
|
{
|
||||||
|
[JsonPropertyName("content")]
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("position")]
|
||||||
|
public int Position { get; set; }
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private static readonly string SHORT_DATE = "yyyy-MM-dd";
|
||||||
|
|
||||||
|
private readonly ConnectionsContext _db = db;
|
||||||
|
private readonly ILogger<SyncUtility> _logger = logger;
|
||||||
|
private readonly HttpClient _http = http;
|
||||||
|
|
||||||
|
public async Task SyncPuzzlesAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Calculating puzzle sync dates");
|
||||||
|
|
||||||
|
// calculate the date ranges for the sync
|
||||||
|
var syncDates = await GetSyncDatesAsync(ct);
|
||||||
|
|
||||||
|
_logger.LogInformation("Syncing puzzles between {start} - {end}", syncDates[0], syncDates[^1]);
|
||||||
|
|
||||||
|
// run the HTTP requests in batches
|
||||||
|
ConcurrentDictionary<string, string> responses = new();
|
||||||
|
foreach (var batch in syncDates.Chunk(5))
|
||||||
{
|
{
|
||||||
[JsonPropertyName("status")]
|
ConcurrentBag<NYTConnectionsPuzzle> batchPuzzles = [];
|
||||||
public string Status { get; set; } = string.Empty;
|
await Task.WhenAll(
|
||||||
[JsonPropertyName("print_date")]
|
batch.Select(x => GetConnectionsResponseAsync(x, ct).ContinueWith(t =>
|
||||||
public string PrintDate { get; set; } = string.Empty;
|
{
|
||||||
[JsonPropertyName("editor")]
|
string? result = t.Result;
|
||||||
public string Editor { get; set; } = string.Empty;
|
if (!string.IsNullOrWhiteSpace(result))
|
||||||
[JsonPropertyName("categories")]
|
|
||||||
public IReadOnlyList<NYTConnectionsPuzzleCategory> Categories { get; set; } = [];
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public string Md5 { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
class NYTConnectionsPuzzleCategory
|
|
||||||
{
|
|
||||||
[JsonPropertyName("title")]
|
|
||||||
public string Title { get; set; } = string.Empty;
|
|
||||||
[JsonPropertyName("cards")]
|
|
||||||
public IReadOnlyList<NYTConnectionsPuzzleCard> Cards { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NYTConnectionsPuzzleCard
|
|
||||||
{
|
|
||||||
[JsonPropertyName("content")]
|
|
||||||
public string Content { get; set; } = string.Empty;
|
|
||||||
[JsonPropertyName("position")]
|
|
||||||
public int Position { get; set; }
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private static readonly string SHORT_DATE = "yyyy-MM-dd";
|
|
||||||
|
|
||||||
private readonly ConnectionsContext _db = db;
|
|
||||||
private readonly ILogger<SyncUtility> _logger = logger;
|
|
||||||
private readonly HttpClient _http = http;
|
|
||||||
|
|
||||||
public async Task SyncPuzzlesAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Calculating puzzle sync dates");
|
|
||||||
|
|
||||||
// calculate the date ranges for the sync
|
|
||||||
var syncDates = await GetSyncDatesAsync(ct);
|
|
||||||
|
|
||||||
_logger.LogInformation("Syncing puzzles between {start} - {end}", syncDates[0], syncDates[^1]);
|
|
||||||
|
|
||||||
// run the HTTP requests in batches
|
|
||||||
ConcurrentDictionary<string, string> responses = new();
|
|
||||||
foreach (var batch in syncDates.Chunk(5))
|
|
||||||
{
|
|
||||||
ConcurrentBag<NYTConnectionsPuzzle> batchPuzzles = [];
|
|
||||||
await Task.WhenAll(
|
|
||||||
batch.Select(x => GetConnectionsResponseAsync(x, ct).ContinueWith(t =>
|
|
||||||
{
|
{
|
||||||
string? result = t.Result;
|
try
|
||||||
if (!string.IsNullOrWhiteSpace(result))
|
|
||||||
{
|
{
|
||||||
try
|
var nytResponseJson = JsonSerializer.Deserialize<NYTConnectionsPuzzle>(result)
|
||||||
{
|
?? throw new InvalidDataException("Connections response deserialized to null");
|
||||||
var nytResponseJson = JsonSerializer.Deserialize<NYTConnectionsPuzzle>(result)
|
|
||||||
?? throw new InvalidDataException("Connections response deserialized to null");
|
|
||||||
|
|
||||||
string md5 = HashUtility.CalculateMD5(result);
|
string md5 = HashUtility.CalculateMD5(result);
|
||||||
nytResponseJson.Md5 = md5;
|
nytResponseJson.Md5 = md5;
|
||||||
batchPuzzles.Add(nytResponseJson);
|
batchPuzzles.Add(nytResponseJson);
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to deserialize Connections response for {date}", x);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Puzzle {date} non-success response, skipping", x);
|
_logger.LogError(ex, "Failed to deserialize Connections response for {date}", x);
|
||||||
}
|
}
|
||||||
}))
|
}
|
||||||
);
|
else
|
||||||
|
|
||||||
foreach (var puzzle in batchPuzzles.OrderBy(x => x.PrintDate))
|
|
||||||
{
|
|
||||||
await UpsertPuzzleDataAsync(puzzle);
|
|
||||||
}
|
|
||||||
await _db.SaveChangesAsync(ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpsertPuzzleDataAsync(NYTConnectionsPuzzle nytPuzzle)
|
|
||||||
{
|
|
||||||
// get a tracking reference to the puzzle matching by print date, either by querying or creating a new entity
|
|
||||||
var puzzle = await _db.CategoriesPuzzles
|
|
||||||
.Include(x => x.Categories)
|
|
||||||
.ThenInclude(x => x.CategoriesPuzzleCards)
|
|
||||||
.FirstOrDefaultAsync(x => x.PrintDate == nytPuzzle.PrintDate);
|
|
||||||
if (puzzle == null)
|
|
||||||
{
|
|
||||||
_logger.LogTrace("No puzzle found for {printDate}, puzzle will be created", nytPuzzle.PrintDate);
|
|
||||||
puzzle = new Database.Entities.CategoriesPuzzle
|
|
||||||
{
|
|
||||||
Categories = [],
|
|
||||||
CreatedDate = DateTime.UtcNow
|
|
||||||
};
|
|
||||||
_db.CategoriesPuzzles.Add(puzzle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the content hash matches, no update needed
|
|
||||||
if (puzzle.ContentMD5 == nytPuzzle.Md5)
|
|
||||||
{
|
|
||||||
_logger.LogTrace("JSON content hash for {printDate} matches, no need for update", nytPuzzle.PrintDate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
puzzle.ContentMD5 = nytPuzzle.Md5;
|
|
||||||
puzzle.PrintDate = nytPuzzle.PrintDate;
|
|
||||||
puzzle.EditorName = nytPuzzle.Editor;
|
|
||||||
puzzle.Index = CalculateConnectionsDayIndex(nytPuzzle.PrintDate);
|
|
||||||
puzzle.Categories ??= [];
|
|
||||||
|
|
||||||
// mark items for deletion and also remove them from here to be readded
|
|
||||||
_db.RemoveRange(puzzle.Categories);
|
|
||||||
puzzle.Categories.Clear();
|
|
||||||
|
|
||||||
// construct the entities
|
|
||||||
int idx = 1;
|
|
||||||
foreach (var nytCategory in nytPuzzle.Categories)
|
|
||||||
{
|
|
||||||
CategoriesCategory category = new()
|
|
||||||
{
|
|
||||||
Color = (CategoriesColor)idx++,
|
|
||||||
Name = nytCategory.Title,
|
|
||||||
CategoriesPuzzle = puzzle,
|
|
||||||
CategoriesPuzzleCards = []
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var nytCard in nytCategory.Cards)
|
|
||||||
{
|
|
||||||
CategoriesCard card = new()
|
|
||||||
{
|
{
|
||||||
Content = nytCard.Content,
|
_logger.LogWarning("Puzzle {date} non-success response, skipping", x);
|
||||||
Position = nytCard.Position,
|
}
|
||||||
Category = category,
|
}))
|
||||||
};
|
);
|
||||||
category.CategoriesPuzzleCards.Add(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
puzzle.Categories.Add(category);
|
foreach (var puzzle in batchPuzzles.OrderBy(x => x.PrintDate))
|
||||||
}
|
|
||||||
// done
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int CalculateConnectionsDayIndex(string printDate)
|
|
||||||
{
|
|
||||||
DateTime connectionsDate = DateTime.ParseExact(printDate, SHORT_DATE, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).Date;
|
|
||||||
return Convert.ToInt32(Math.Max((connectionsDate - Constants.ConnectionsStartDate).TotalDays, 0)) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string?> GetConnectionsResponseAsync(string printDate, CancellationToken ct)
|
|
||||||
{
|
|
||||||
string url = $"https://www.nytimes.com/svc/connections/v2/{printDate}.json";
|
|
||||||
using var resp = await _http.GetAsync(url, ct);
|
|
||||||
if (resp == null || !resp.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
return null;
|
await UpsertPuzzleDataAsync(puzzle);
|
||||||
}
|
}
|
||||||
string responseContent = await resp.Content.ReadAsStringAsync(ct);
|
await _db.SaveChangesAsync(ct);
|
||||||
return responseContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IReadOnlyList<string>> GetSyncDatesAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
// query the last puzzle we have in the database
|
|
||||||
string? lastSyncedPuzzleDate = await _db.CategoriesPuzzles.AsNoTracking()
|
|
||||||
.OrderByDescending(x => x.PrintDate)
|
|
||||||
.Select(x => x.PrintDate)
|
|
||||||
.FirstOrDefaultAsync(cancellationToken: ct);
|
|
||||||
|
|
||||||
// calculate the starting date of the sync
|
|
||||||
string startDate;
|
|
||||||
|
|
||||||
// if no puzzle was synced before, we use the start day of connections as a start (we want to sync every puzzle ever)
|
|
||||||
if (string.IsNullOrWhiteSpace(lastSyncedPuzzleDate))
|
|
||||||
{
|
|
||||||
startDate = Constants.ConnectionsStartDate.ToString(SHORT_DATE);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string todayPrintDate = DateTimeOffset.UtcNow.UtcDateTime.ToString(SHORT_DATE);
|
|
||||||
// if we have a puzzle, we check the latest print date we have
|
|
||||||
// if the print date is earlier than today's date, we use that day as a base
|
|
||||||
// if the print date is after today, we use today as a base
|
|
||||||
if (lastSyncedPuzzleDate.CompareTo(todayPrintDate) < 0)
|
|
||||||
{
|
|
||||||
startDate = lastSyncedPuzzleDate;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
startDate = todayPrintDate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// construct a list of dates
|
|
||||||
List<string> dates = [startDate];
|
|
||||||
|
|
||||||
// we iterate on every day between the start date and UTC tomorrow (this should handle +12 timezones as well)
|
|
||||||
DateTime syncBeginDate = DateTime.ParseExact(startDate, SHORT_DATE, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).Date;
|
|
||||||
DateTime syncEndDate;
|
|
||||||
|
|
||||||
// try to find the latest date that is currently going on in the world
|
|
||||||
TimeZoneInfo? latestTimezone = TimezoneUtility.GetLatestTimezoneOnSystem();
|
|
||||||
if (latestTimezone != null)
|
|
||||||
{
|
|
||||||
DateTime currentDateInLatestTimezone = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, latestTimezone);
|
|
||||||
syncEndDate = new DateTime(currentDateInLatestTimezone.Year, currentDateInLatestTimezone.Month, currentDateInLatestTimezone.Day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
// default to UTC date + 1 day
|
|
||||||
else
|
|
||||||
{
|
|
||||||
syncEndDate = DateTime.UtcNow.Date.AddDays(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var date in Enumerable.Repeat(0, Convert.ToInt32((syncEndDate - syncBeginDate).TotalDays)).Select((_, idx) => syncBeginDate.AddDays(idx + 1)))
|
|
||||||
{
|
|
||||||
dates.Add(date.ToString(SHORT_DATE));
|
|
||||||
}
|
|
||||||
|
|
||||||
// done
|
|
||||||
return dates;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UpsertPuzzleDataAsync(NYTConnectionsPuzzle nytPuzzle)
|
||||||
|
{
|
||||||
|
// get a tracking reference to the puzzle matching by print date, either by querying or creating a new entity
|
||||||
|
var puzzle = await _db.ConnectionsPuzzles
|
||||||
|
.Include(x => x.Categories)
|
||||||
|
.ThenInclude(x => x.Cards)
|
||||||
|
.FirstOrDefaultAsync(x => x.PrintDate == nytPuzzle.PrintDate);
|
||||||
|
if (puzzle == null)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("No puzzle found for {printDate}, puzzle will be created", nytPuzzle.PrintDate);
|
||||||
|
puzzle = new Database.Entities.ConnectionsPuzzle
|
||||||
|
{
|
||||||
|
Categories = [],
|
||||||
|
CreatedDate = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
_db.ConnectionsPuzzles.Add(puzzle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the content hash matches, no update needed
|
||||||
|
if (puzzle.ContentMD5 == nytPuzzle.Md5)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("JSON content hash for {printDate} matches, no need for update", nytPuzzle.PrintDate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
puzzle.ContentMD5 = nytPuzzle.Md5;
|
||||||
|
puzzle.PrintDate = nytPuzzle.PrintDate;
|
||||||
|
puzzle.EditorName = nytPuzzle.Editor;
|
||||||
|
puzzle.Index = CalculateConnectionsDayIndex(nytPuzzle.PrintDate);
|
||||||
|
puzzle.Categories ??= [];
|
||||||
|
|
||||||
|
// mark items for deletion and also remove them from here to be readded
|
||||||
|
_db.RemoveRange(puzzle.Categories);
|
||||||
|
puzzle.Categories.Clear();
|
||||||
|
|
||||||
|
// construct the entities
|
||||||
|
int idx = 1;
|
||||||
|
foreach (var nytCategory in nytPuzzle.Categories)
|
||||||
|
{
|
||||||
|
ConnectionsCategory category = new()
|
||||||
|
{
|
||||||
|
Color = (ConnectionsColor)idx++,
|
||||||
|
Name = nytCategory.Title,
|
||||||
|
ConnectionsPuzzle = puzzle,
|
||||||
|
Cards = []
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var nytCard in nytCategory.Cards)
|
||||||
|
{
|
||||||
|
ConnectionsCard card = new()
|
||||||
|
{
|
||||||
|
Content = nytCard.Content,
|
||||||
|
Position = nytCard.Position,
|
||||||
|
Category = category,
|
||||||
|
};
|
||||||
|
category.Cards.Add(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
puzzle.Categories.Add(category);
|
||||||
|
}
|
||||||
|
// done
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CalculateConnectionsDayIndex(string printDate)
|
||||||
|
{
|
||||||
|
DateTime connectionsDate = DateTime.ParseExact(printDate, SHORT_DATE, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).Date;
|
||||||
|
return Convert.ToInt32(Math.Max((connectionsDate - Constants.ConnectionsStartDate).TotalDays, 0)) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> GetConnectionsResponseAsync(string printDate, CancellationToken ct)
|
||||||
|
{
|
||||||
|
string url = $"https://www.nytimes.com/svc/connections/v2/{printDate}.json";
|
||||||
|
using var resp = await _http.GetAsync(url, ct);
|
||||||
|
if (resp == null || !resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
string responseContent = await resp.Content.ReadAsStringAsync(ct);
|
||||||
|
return responseContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<string>> GetSyncDatesAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
// query the last puzzle we have in the database
|
||||||
|
string? lastSyncedPuzzleDate = await _db.ConnectionsPuzzles.AsNoTracking()
|
||||||
|
.OrderByDescending(x => x.PrintDate)
|
||||||
|
.Select(x => x.PrintDate)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||||
|
|
||||||
|
// calculate the starting date of the sync
|
||||||
|
string startDate;
|
||||||
|
|
||||||
|
// if no puzzle was synced before, we use the start day of connections as a start (we want to sync every puzzle ever)
|
||||||
|
if (string.IsNullOrWhiteSpace(lastSyncedPuzzleDate))
|
||||||
|
{
|
||||||
|
startDate = Constants.ConnectionsStartDate.ToString(SHORT_DATE);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string todayPrintDate = DateTimeOffset.UtcNow.UtcDateTime.ToString(SHORT_DATE);
|
||||||
|
// if we have a puzzle, we check the latest print date we have
|
||||||
|
// if the print date is earlier than today's date, we use that day as a base
|
||||||
|
// if the print date is after today, we use today as a base
|
||||||
|
if (lastSyncedPuzzleDate.CompareTo(todayPrintDate) < 0)
|
||||||
|
{
|
||||||
|
startDate = lastSyncedPuzzleDate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
startDate = todayPrintDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// construct a list of dates
|
||||||
|
List<string> dates = [startDate];
|
||||||
|
|
||||||
|
// we iterate on every day between the start date and UTC tomorrow (this should handle +12 timezones as well)
|
||||||
|
DateTime syncBeginDate = DateTime.ParseExact(startDate, SHORT_DATE, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).Date;
|
||||||
|
DateTime syncEndDate;
|
||||||
|
|
||||||
|
// try to find the latest date that is currently going on in the world
|
||||||
|
TimeZoneInfo? latestTimezone = TimezoneUtility.GetLatestTimezoneOnSystem();
|
||||||
|
if (latestTimezone != null)
|
||||||
|
{
|
||||||
|
DateTime currentDateInLatestTimezone = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, latestTimezone);
|
||||||
|
syncEndDate = new DateTime(currentDateInLatestTimezone.Year, currentDateInLatestTimezone.Month, currentDateInLatestTimezone.Day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
// default to UTC date + 1 day
|
||||||
|
else
|
||||||
|
{
|
||||||
|
syncEndDate = DateTime.UtcNow.Date.AddDays(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var date in Enumerable.Repeat(0, Convert.ToInt32((syncEndDate - syncBeginDate).TotalDays)).Select((_, idx) => syncBeginDate.AddDays(idx + 1)))
|
||||||
|
{
|
||||||
|
dates.Add(date.ToString(SHORT_DATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
// done
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
namespace ConnectionsAPI.Utility
|
namespace ConnectionsAPI.Utility;
|
||||||
|
public static class TimezoneUtility
|
||||||
{
|
{
|
||||||
public static class TimezoneUtility
|
public static TimeZoneInfo? GetLatestTimezoneOnSystem() =>
|
||||||
{
|
TimeZoneInfo.GetSystemTimeZones()
|
||||||
public static TimeZoneInfo? GetLatestTimezoneOnSystem() =>
|
.OrderByDescending(x => x.GetUtcOffset(DateTime.UtcNow))
|
||||||
TimeZoneInfo.GetSystemTimeZones()
|
.FirstOrDefault();
|
||||||
.OrderByDescending(x => x.GetUtcOffset(DateTime.UtcNow))
|
|
||||||
.FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user