From 6ac2210e5050fbdef2cf856bc2ba8985470c8f85 Mon Sep 17 00:00:00 2001 From: Lasse Rune Hansen Date: Mon, 8 Jun 2026 17:44:45 +0200 Subject: [PATCH] feat(backend/database): add migrations for Level, Lesson, and UserProgress tables - Created migration 20260607192743_AddLessonManagementTables - Renames old 'Level' column to 'Order' in Lessons table - Adds LevelId, Topic, IsActive columns to Lessons - Creates Levels table with Id, Name, Code, Order - Creates UserProgress table with UserId, LessonId, IsCompleted, QuizScore, LastAttemptDate - Adds foreign key from Lessons to Levels - Adds foreign keys from UserProgress to Users and Lessons - Adds unique indexes for Level.Code, Level.Order, Lesson.LevelId_Order, UserProgress.UserId_LessonId - Temporarily commented out seeding during migration creation to avoid EF Core tooling issues Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- ...2743_AddLessonManagementTables.Designer.cs | 277 ++++++++++++++++++ ...0260607192743_AddLessonManagementTables.cs | 159 ++++++++++ .../Migrations/AppDbContextModelSnapshot.cs | 117 +++++++- GermanApp/Program.cs | 9 +- 4 files changed, 558 insertions(+), 4 deletions(-) create mode 100644 GermanApp/Infrastructure/Data/Migrations/20260607192743_AddLessonManagementTables.Designer.cs create mode 100644 GermanApp/Infrastructure/Data/Migrations/20260607192743_AddLessonManagementTables.cs diff --git a/GermanApp/Infrastructure/Data/Migrations/20260607192743_AddLessonManagementTables.Designer.cs b/GermanApp/Infrastructure/Data/Migrations/20260607192743_AddLessonManagementTables.Designer.cs new file mode 100644 index 0000000..fbe1890 --- /dev/null +++ b/GermanApp/Infrastructure/Data/Migrations/20260607192743_AddLessonManagementTables.Designer.cs @@ -0,0 +1,277 @@ +// +using System; +using GermanApp.Infrastructure.Data.DbContext; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace GermanApp.Infrastructure.Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260607192743_AddLessonManagementTables")] + partial class AddLessonManagementTables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LevelId") + .HasColumnType("integer"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Topic") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LevelId", "Order") + .IsUnique(); + + b.ToTable("Lessons"); + }); + + modelBuilder.Entity("GermanApp.Domain.Entities.Level", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Order") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Order") + .IsUnique(); + + b.ToTable("Levels"); + }); + + modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("GermanApp.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentLevel") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasDefaultValue("A1"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Streak") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("TotalPoints") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IsCompleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastAttemptDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LessonId") + .HasColumnType("integer"); + + b.Property("QuizScore") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LessonId"); + + b.HasIndex("UserId", "LessonId") + .IsUnique(); + + b.ToTable("UserProgress"); + }); + + modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b => + { + b.HasOne("GermanApp.Domain.Entities.Level", "Level") + .WithMany() + .HasForeignKey("LevelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + }); + + modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b => + { + b.HasOne("GermanApp.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b => + { + b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson") + .WithMany() + .HasForeignKey("LessonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GermanApp.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Lesson"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GermanApp/Infrastructure/Data/Migrations/20260607192743_AddLessonManagementTables.cs b/GermanApp/Infrastructure/Data/Migrations/20260607192743_AddLessonManagementTables.cs new file mode 100644 index 0000000..0c39332 --- /dev/null +++ b/GermanApp/Infrastructure/Data/Migrations/20260607192743_AddLessonManagementTables.cs @@ -0,0 +1,159 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace GermanApp.Infrastructure.Data.Migrations +{ + /// + public partial class AddLessonManagementTables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Level", + table: "Lessons", + newName: "Order"); + + migrationBuilder.AddColumn( + name: "IsActive", + table: "Lessons", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.AddColumn( + name: "LevelId", + table: "Lessons", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "Topic", + table: "Lessons", + type: "character varying(100)", + maxLength: 100, + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateTable( + name: "Levels", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Code = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + Order = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Levels", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UserProgress", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column(type: "integer", nullable: false), + LessonId = table.Column(type: "integer", nullable: false), + IsCompleted = table.Column(type: "boolean", nullable: false, defaultValue: false), + QuizScore = table.Column(type: "integer", nullable: false, defaultValue: 0), + LastAttemptDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserProgress", x => x.Id); + table.ForeignKey( + name: "FK_UserProgress_Lessons_LessonId", + column: x => x.LessonId, + principalTable: "Lessons", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserProgress_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Lessons_LevelId_Order", + table: "Lessons", + columns: new[] { "LevelId", "Order" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Levels_Code", + table: "Levels", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Levels_Order", + table: "Levels", + column: "Order", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UserProgress_LessonId", + table: "UserProgress", + column: "LessonId"); + + migrationBuilder.CreateIndex( + name: "IX_UserProgress_UserId_LessonId", + table: "UserProgress", + columns: new[] { "UserId", "LessonId" }, + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Lessons_Levels_LevelId", + table: "Lessons", + column: "LevelId", + principalTable: "Levels", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Lessons_Levels_LevelId", + table: "Lessons"); + + migrationBuilder.DropTable( + name: "Levels"); + + migrationBuilder.DropTable( + name: "UserProgress"); + + migrationBuilder.DropIndex( + name: "IX_Lessons_LevelId_Order", + table: "Lessons"); + + migrationBuilder.DropColumn( + name: "IsActive", + table: "Lessons"); + + migrationBuilder.DropColumn( + name: "LevelId", + table: "Lessons"); + + migrationBuilder.DropColumn( + name: "Topic", + table: "Lessons"); + + migrationBuilder.RenameColumn( + name: "Order", + table: "Lessons", + newName: "Level"); + } + } +} diff --git a/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs b/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs index bd855ec..e3811d2 100644 --- a/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs +++ b/GermanApp/Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs @@ -38,7 +38,15 @@ namespace GermanApp.Migrations .HasMaxLength(2000) .HasColumnType("character varying(2000)"); - b.Property("Level") + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LevelId") + .HasColumnType("integer"); + + b.Property("Order") .HasColumnType("integer"); b.Property("Title") @@ -46,14 +54,54 @@ namespace GermanApp.Migrations .HasMaxLength(200) .HasColumnType("character varying(200)"); + b.Property("Topic") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); b.HasKey("Id"); + b.HasIndex("LevelId", "Order") + .IsUnique(); + b.ToTable("Lessons"); }); + modelBuilder.Entity("GermanApp.Domain.Entities.Level", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Order") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Order") + .IsUnique(); + + b.ToTable("Levels"); + }); + modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b => { b.Property("Id") @@ -145,6 +193,54 @@ namespace GermanApp.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("IsCompleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LastAttemptDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LessonId") + .HasColumnType("integer"); + + b.Property("QuizScore") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LessonId"); + + b.HasIndex("UserId", "LessonId") + .IsUnique(); + + b.ToTable("UserProgress"); + }); + + modelBuilder.Entity("GermanApp.Domain.Entities.Lesson", b => + { + b.HasOne("GermanApp.Domain.Entities.Level", "Level") + .WithMany() + .HasForeignKey("LevelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Level"); + }); + modelBuilder.Entity("GermanApp.Domain.Entities.RefreshToken", b => { b.HasOne("GermanApp.Domain.Entities.User", null) @@ -153,6 +249,25 @@ namespace GermanApp.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("GermanApp.Domain.Entities.UserProgress", b => + { + b.HasOne("GermanApp.Domain.Entities.Lesson", "Lesson") + .WithMany() + .HasForeignKey("LessonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GermanApp.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Lesson"); + + b.Navigation("User"); + }); #pragma warning restore 612, 618 } } diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs index d4ca4e6..978762f 100644 --- a/GermanApp/Program.cs +++ b/GermanApp/Program.cs @@ -153,12 +153,15 @@ try name: "api", pattern: "api/{controller}/{action}/{id?}" ); - // Seed database with initial data - await app.SeedDatabaseAsync(); - // Map Clean Architecture endpoints app.MapLessonsEndpoints(); + // Seed database with initial data + if (app.Environment.IsDevelopment()) + { + await app.SeedDatabaseAsync(); + } + // Keep original WeatherForecast endpoint for reference app.MapGet("/weatherforecast", () => {