diff --git a/DATABASE_NAMING_CONVENTION_PLAN.md b/DATABASE_NAMING_CONVENTION_PLAN.md new file mode 100644 index 0000000..2569f19 --- /dev/null +++ b/DATABASE_NAMING_CONVENTION_PLAN.md @@ -0,0 +1,213 @@ +# 資料庫命名規範統一計劃 + +## 📋 計劃概述 + +本計劃旨在解決 DramaLing 專案中資料庫欄位命名不一致的問題,將所有資料庫欄位統一為 `snake_case` 命名規範。 + +## 🔍 現況分析 + +### 問題描述 +目前資料庫中同時存在兩種命名方式: +- **PascalCase**: `Example`, `Word`, `Translation`, `Definition`, `Name`, `Color` 等 +- **snake_case**: `user_id`, `created_at`, `is_favorite`, `part_of_speech` 等 + +### 問題根源 +1. **歷史遺留**: 早期遷移沒有統一配置欄位命名 +2. **不完整配置**: 部分屬性缺少 `.HasColumnName()` 映射 +3. **維護疏漏**: 新增欄位時沒有遵循統一規範 + +### 已修復項目 ✅ +- ✅ `Flashcard.Word` → `word` +- ✅ `Flashcard.Translation` → `translation` +- ✅ `Flashcard.Definition` → `definition` +- ✅ `Flashcard.Pronunciation` → `pronunciation` +- ✅ `Flashcard.Example` → `example` (原始問題) + +## 🎯 統一規範標準 + +### 命名規則 +| 層級 | 命名方式 | 範例 | 說明 | +|------|----------|------|------| +| **C# 實體屬性** | PascalCase | `UserId`, `CreatedAt`, `ExampleTranslation` | 符合 C# 慣例 | +| **資料庫欄位** | snake_case | `user_id`, `created_at`, `example_translation` | 符合資料庫慣例 | +| **表格名稱** | snake_case | `flashcards`, `user_profiles`, `daily_stats` | 保持一致性 | + +### 映射規則 +```csharp +// 所有屬性都需要明確映射 +entity.Property(e => e.PropertyName).HasColumnName("property_name"); +``` + +## 📝 待修復項目清單 + +### 1. Tag 實體 +```csharp +// 目前缺少的映射: +public Guid Id { get; set; } // → "id" +public string Name { get; set; } // → "name" +public string Color { get; set; } // → "color" +``` + +### 2. ErrorReport 實體 +```csharp +// 目前缺少的映射: +public Guid Id { get; set; } // → "id" +public string? Description { get; set; } // → "description" +public string Status { get; set; } // → "status" +``` + +### 3. DailyStats 實體 +```csharp +// 目前缺少的映射: +public Guid Id { get; set; } // → "id" +public DateTime Date { get; set; } // → "date" +``` + +### 4. 其他實體 +需要檢查以下實體是否有遺漏的映射: +- `SentenceAnalysisCache` +- `WordQueryUsageStats` +- `ExampleImage` +- `ImageGenerationRequest` +- `OptionsVocabulary` + +### 5. 通用 Id 欄位 +所有實體的 `Id` 屬性都應該映射為 `id` + +## 🚀 執行步驟 + +### 階段一:DbContext 配置更新 +1. **補充 Tag 實體配置** + ```csharp + private void ConfigureTagEntities(ModelBuilder modelBuilder) + { + var tagEntity = modelBuilder.Entity(); + tagEntity.Property(t => t.Id).HasColumnName("id"); + tagEntity.Property(t => t.Name).HasColumnName("name"); + tagEntity.Property(t => t.Color).HasColumnName("color"); + // 其他現有配置... + } + ``` + +2. **補充其他實體配置** + - 更新 `ConfigureErrorReportEntity` + - 更新 `ConfigureDailyStatsEntity` + - 新增其他實體的配置方法 + +### 階段二:資料庫遷移 +1. **建立遷移** + ```bash + dotnet ef migrations add CompleteSnakeCaseNaming + ``` + +2. **套用遷移** + ```bash + dotnet ef database update + ``` + +### 階段三:驗證與測試 +1. **檢查資料庫結構** + ```sql + .schema table_name + ``` + +2. **測試應用程式功能** + - API 端點測試 + - 資料查詢測試 + - 完整功能驗證 + +## 📋 檢核清單 + +### 配置檢核 +- [ ] 所有實體的 `Id` 屬性都有 `.HasColumnName("id")` +- [ ] 所有多單字屬性都使用 snake_case(如 `CreatedAt` → `created_at`) +- [ ] 所有布林屬性都使用 `is_` 前綴(如 `IsActive` → `is_active`) +- [ ] 外鍵屬性都使用 `_id` 後綴(如 `UserId` → `user_id`) + +### 遷移檢核 +- [ ] 遷移檔案正確生成 +- [ ] SQL 指令正確(RENAME COLUMN) +- [ ] 沒有資料遺失風險 +- [ ] 回滾計劃準備完成 + +### 測試檢核 +- [ ] 所有 API 端點正常運作 +- [ ] 資料查詢結果正確 +- [ ] 無效能退化 +- [ ] 前端功能正常 + +## 🔧 長期維護建議 + +### 1. 編碼規範 +建立明確的編碼規範文檔: +```markdown +## 資料庫命名規範 +- 所有新增的實體屬性都必須配置 `.HasColumnName()` +- 資料庫欄位名稱統一使用 snake_case +- 布林欄位使用 `is_` 前綴 +- 外鍵欄位使用 `_id` 後綴 +``` + +### 2. Code Review 檢查點 +在 PR 審查時檢查: +- 新增實體是否有完整的欄位映射配置 +- 遷移檔案是否符合命名規範 +- 是否需要更新相關文檔 + +### 3. 自動化檢查 +考慮實施: +- **Pre-commit Hook**: 檢查新增的 DbContext 配置 +- **CI/CD 檢查**: 驗證遷移檔案的正確性 +- **單元測試**: 確保所有實體都有正確的欄位映射 + +### 4. 全局慣例配置(進階選項) +可以考慮使用 EF Core 的全局慣例: +```csharp +protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) +{ + configurationBuilder.Properties() + .HaveColumnName(propertyInfo => propertyInfo.Name.ToSnakeCase()); +} +``` + +## 📊 影響評估 + +### 優點 +- ✅ 統一的命名規範 +- ✅ 更好的可維護性 +- ✅ 避免開發混淆 +- ✅ 符合業界標準 + +### 風險 +- ⚠️ 需要資料庫遷移 +- ⚠️ 可能影響現有查詢 +- ⚠️ 需要充分測試 + +### 緩解措施 +- 📋 充分的測試計劃 +- 🔄 準備回滾方案 +- 📝 詳細的變更文檔 +- 👥 團隊溝通協調 + +## 🗓️ 執行時間表 + +| 階段 | 預估時間 | 責任人 | 狀態 | +|------|----------|--------|------| +| 現況分析 | 0.5 天 | 開發團隊 | ✅ 完成 | +| 配置更新 | 1 天 | 後端開發 | 🚧 進行中 | +| 遷移建立 | 0.5 天 | 後端開發 | ⏳ 待執行 | +| 測試驗證 | 1 天 | 全團隊 | ⏳ 待執行 | +| 部署上線 | 0.5 天 | DevOps | ⏳ 待執行 | + +## 📞 聯絡資訊 + +如有問題或需要協助,請聯絡: +- **技術負責人**: [待填入] +- **專案經理**: [待填入] +- **QA 負責人**: [待填入] + +--- + +**文件版本**: v1.0 +**最後更新**: 2025-09-30 +**建立人**: Claude Code Assistant \ No newline at end of file diff --git a/backend/DramaLing.Api/Controllers/AIController.cs b/backend/DramaLing.Api/Controllers/AIController.cs index 578fad9..f5870dd 100644 --- a/backend/DramaLing.Api/Controllers/AIController.cs +++ b/backend/DramaLing.Api/Controllers/AIController.cs @@ -6,19 +6,16 @@ using System.Diagnostics; namespace DramaLing.Api.Controllers; -[ApiController] [Route("api/ai")] -public class AIController : ControllerBase +public class AIController : BaseController { private readonly IAnalysisService _analysisService; - private readonly ILogger _logger; public AIController( IAnalysisService analysisService, - ILogger logger) + ILogger logger) : base(logger) { _analysisService = analysisService; - _logger = logger; } /// @@ -28,7 +25,7 @@ public class AIController : ControllerBase /// 分析結果 [HttpPost("analyze-sentence")] [AllowAnonymous] - public async Task> AnalyzeSentence( + public async Task AnalyzeSentence( [FromBody] SentenceAnalysisRequest request) { var requestId = Guid.NewGuid().ToString(); @@ -45,9 +42,7 @@ public class AIController : ControllerBase // Input validation if (!ModelState.IsValid) { - return BadRequest(CreateErrorResponse("INVALID_INPUT", "輸入格式錯誤", - ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(), - requestId)); + return HandleModelStateErrors(); } // 使用帶快取的分析服務 @@ -61,27 +56,29 @@ public class AIController : ControllerBase _logger.LogInformation("Sentence analysis completed for request {RequestId} in {ElapsedMs}ms", requestId, stopwatch.ElapsedMilliseconds); - return Ok(new SentenceAnalysisResponse + var response = new SentenceAnalysisResponse { Success = true, ProcessingTime = stopwatch.Elapsed.TotalSeconds, Data = analysisData - }); + }; + + return SuccessResponse(response); } catch (ArgumentException ex) { _logger.LogWarning(ex, "Invalid input for request {RequestId}", requestId); - return BadRequest(CreateErrorResponse("INVALID_INPUT", ex.Message, null, requestId)); + return ErrorResponse("INVALID_INPUT", ex.Message, null, 400); } catch (InvalidOperationException ex) { _logger.LogError(ex, "AI service error for request {RequestId}", requestId); - return StatusCode(500, CreateErrorResponse("AI_SERVICE_ERROR", "AI服務暫時不可用", null, requestId)); + return ErrorResponse("AI_SERVICE_ERROR", "AI服務暫時不可用", null, 500); } catch (Exception ex) { _logger.LogError(ex, "Unexpected error processing request {RequestId}", requestId); - return StatusCode(500, CreateErrorResponse("INTERNAL_ERROR", "伺服器內部錯誤", null, requestId)); + return ErrorResponse("INTERNAL_ERROR", "伺服器內部錯誤", null, 500); } } @@ -90,15 +87,17 @@ public class AIController : ControllerBase /// [HttpGet("health")] [AllowAnonymous] - public ActionResult GetHealth() + public IActionResult GetHealth() { - return Ok(new + var healthData = new { Status = "Healthy", Service = "AI Analysis Service", Timestamp = DateTime.UtcNow, Version = "1.0" - }); + }; + + return SuccessResponse(healthData); } /// @@ -106,59 +105,28 @@ public class AIController : ControllerBase /// [HttpGet("stats")] [AllowAnonymous] - public async Task GetAnalysisStats() + public async Task GetAnalysisStats() { try { var stats = await _analysisService.GetAnalysisStatsAsync(); - return Ok(new + var statsData = new { - Success = true, - Data = new - { - TotalAnalyses = stats.TotalAnalyses, - CachedAnalyses = stats.CachedAnalyses, - CacheHitRate = stats.CacheHitRate, - AverageResponseTimeMs = stats.AverageResponseTimeMs, - LastAnalysisAt = stats.LastAnalysisAt, - ProviderUsage = stats.ProviderUsageStats - } - }); + TotalAnalyses = stats.TotalAnalyses, + CachedAnalyses = stats.CachedAnalyses, + CacheHitRate = stats.CacheHitRate, + AverageResponseTimeMs = stats.AverageResponseTimeMs, + LastAnalysisAt = stats.LastAnalysisAt, + ProviderUsage = stats.ProviderUsageStats + }; + + return SuccessResponse(statsData); } catch (Exception ex) { _logger.LogError(ex, "Error getting analysis stats"); - return StatusCode(500, new { Success = false, Error = "無法取得統計資訊" }); + return ErrorResponse("INTERNAL_ERROR", "無法取得統計資訊"); } } - private ApiErrorResponse CreateErrorResponse(string code, string message, object? details, string requestId) - { - var suggestions = GetSuggestionsForError(code); - - return new ApiErrorResponse - { - Success = false, - Error = new ApiError - { - Code = code, - Message = message, - Details = details, - Suggestions = suggestions - }, - RequestId = requestId, - Timestamp = DateTime.UtcNow - }; - } - - private List GetSuggestionsForError(string errorCode) - { - return errorCode switch - { - "INVALID_INPUT" => new List { "請檢查輸入格式", "確保文本長度在限制內" }, - "RATE_LIMIT_EXCEEDED" => new List { "升級到Premium帳戶以獲得無限使用", "明天重新嘗試" }, - "AI_SERVICE_ERROR" => new List { "請稍後重試", "如果問題持續,請聯繫客服" }, - _ => new List { "請稍後重試" } - }; - } } \ No newline at end of file diff --git a/backend/DramaLing.Api/Controllers/AnalysisController.cs b/backend/DramaLing.Api/Controllers/AnalysisController.cs new file mode 100644 index 0000000..e8d7b4f --- /dev/null +++ b/backend/DramaLing.Api/Controllers/AnalysisController.cs @@ -0,0 +1,124 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using DramaLing.Api.Models.DTOs; +using DramaLing.Api.Services; +using System.Diagnostics; + +namespace DramaLing.Api.Controllers; + +[Route("api/analysis")] +[AllowAnonymous] +public class AnalysisController : BaseController +{ + private readonly IAnalysisService _analysisService; + + public AnalysisController( + IAnalysisService analysisService, + ILogger logger) : base(logger) + { + _analysisService = analysisService; + } + + /// + /// 智能分析英文句子 + /// + /// 分析請求 + /// 分析結果 + [HttpPost("analyze")] + public async Task AnalyzeSentence([FromBody] SentenceAnalysisRequest request) + { + var requestId = Guid.NewGuid().ToString(); + var stopwatch = Stopwatch.StartNew(); + + try + { + // Input validation + if (!ModelState.IsValid) + { + return HandleModelStateErrors(); + } + + _logger.LogInformation("Processing sentence analysis request {RequestId}", requestId); + + // 使用帶快取的分析服務 + var options = request.Options ?? new AnalysisOptions(); + var analysisData = await _analysisService.AnalyzeSentenceAsync( + request.InputText, options); + + stopwatch.Stop(); + analysisData.Metadata.ProcessingDate = DateTime.UtcNow; + + _logger.LogInformation("Sentence analysis completed for request {RequestId} in {ElapsedMs}ms", + requestId, stopwatch.ElapsedMilliseconds); + + var response = new SentenceAnalysisResponse + { + Success = true, + ProcessingTime = stopwatch.Elapsed.TotalSeconds, + Data = analysisData + }; + + return SuccessResponse(response); + } + catch (ArgumentException ex) + { + _logger.LogWarning(ex, "Invalid input for request {RequestId}", requestId); + return ErrorResponse("INVALID_INPUT", ex.Message, null, 400); + } + catch (InvalidOperationException ex) + { + _logger.LogError(ex, "AI service error for request {RequestId}", requestId); + return ErrorResponse("AI_SERVICE_ERROR", "AI服務暫時不可用", null, 500); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error processing request {RequestId}", requestId); + return ErrorResponse("INTERNAL_ERROR", "伺服器內部錯誤", null, 500); + } + } + + /// + /// 健康檢查端點 + /// + [HttpGet("health")] + public IActionResult GetHealth() + { + var healthData = new + { + Status = "Healthy", + Service = "Analysis Service", + Timestamp = DateTime.UtcNow, + Version = "1.0" + }; + + return SuccessResponse(healthData); + } + + /// + /// 取得分析統計資訊 + /// + [HttpGet("stats")] + public async Task GetAnalysisStats() + { + try + { + var stats = await _analysisService.GetAnalysisStatsAsync(); + var statsData = new + { + TotalAnalyses = stats.TotalAnalyses, + CachedAnalyses = stats.CachedAnalyses, + CacheHitRate = stats.CacheHitRate, + AverageResponseTimeMs = stats.AverageResponseTimeMs, + LastAnalysisAt = stats.LastAnalysisAt, + ProviderUsage = stats.ProviderUsageStats + }; + + return SuccessResponse(statsData); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting analysis stats"); + return ErrorResponse("INTERNAL_ERROR", "無法取得統計資訊"); + } + } +} \ No newline at end of file diff --git a/backend/DramaLing.Api/Controllers/AudioController.cs b/backend/DramaLing.Api/Controllers/AudioController.cs index d21cabc..132733e 100644 --- a/backend/DramaLing.Api/Controllers/AudioController.cs +++ b/backend/DramaLing.Api/Controllers/AudioController.cs @@ -5,23 +5,20 @@ using DramaLing.Api.Services; namespace DramaLing.Api.Controllers; -[ApiController] [Route("api/[controller]")] [Authorize] -public class AudioController : ControllerBase +public class AudioController : BaseController { private readonly IAudioCacheService _audioCacheService; private readonly IAzureSpeechService _speechService; - private readonly ILogger _logger; public AudioController( IAudioCacheService audioCacheService, IAzureSpeechService speechService, - ILogger logger) + ILogger logger) : base(logger) { _audioCacheService = audioCacheService; _speechService = speechService; - _logger = logger; } /// @@ -30,7 +27,7 @@ public class AudioController : ControllerBase /// TTS request parameters /// Audio URL and metadata [HttpPost("tts")] - public async Task> GenerateAudio([FromBody] TTSRequest request) + public async Task GenerateAudio([FromBody] TTSRequest request) { try { @@ -70,10 +67,10 @@ public class AudioController : ControllerBase if (!string.IsNullOrEmpty(response.Error)) { - return StatusCode(500, response); + return ErrorResponse("TTS_ERROR", response.Error, null, 500); } - return Ok(response); + return SuccessResponse(response); } catch (Exception ex) { diff --git a/backend/DramaLing.Api/Controllers/AuthController.cs b/backend/DramaLing.Api/Controllers/AuthController.cs index 6a75910..3af6944 100644 --- a/backend/DramaLing.Api/Controllers/AuthController.cs +++ b/backend/DramaLing.Api/Controllers/AuthController.cs @@ -12,26 +12,21 @@ using Microsoft.IdentityModel.Tokens; namespace DramaLing.Api.Controllers; -[ApiController] [Route("api/[controller]")] -public class AuthController : ControllerBase +public class AuthController : BaseController { private readonly DramaLingDbContext _context; - private readonly IAuthService _authService; - private readonly ILogger _logger; public AuthController( DramaLingDbContext context, IAuthService authService, - ILogger logger) + ILogger logger) : base(logger, authService) { _context = context; - _authService = authService; - _logger = logger; } [HttpPost("register")] - public async Task Register([FromBody] RegisterRequest request) + public async Task Register([FromBody] RegisterRequest request) { try { diff --git a/backend/DramaLing.Api/Controllers/ImageGenerationController.cs b/backend/DramaLing.Api/Controllers/ImageGenerationController.cs index b8dc3de..81f0aa2 100644 --- a/backend/DramaLing.Api/Controllers/ImageGenerationController.cs +++ b/backend/DramaLing.Api/Controllers/ImageGenerationController.cs @@ -7,19 +7,16 @@ using System.Security.Claims; namespace DramaLing.Api.Controllers; [Route("api/[controller]")] -[ApiController] [AllowAnonymous] // 暫時移除認證要求,與 FlashcardsController 保持一致 -public class ImageGenerationController : ControllerBase +public class ImageGenerationController : BaseController { private readonly IImageGenerationOrchestrator _orchestrator; - private readonly ILogger _logger; public ImageGenerationController( IImageGenerationOrchestrator orchestrator, - ILogger logger) + ILogger logger) : base(logger) { _orchestrator = orchestrator ?? throw new ArgumentNullException(nameof(orchestrator)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// @@ -43,17 +40,17 @@ public class ImageGenerationController : ControllerBase var result = await _orchestrator.StartGenerationAsync(flashcardId, request); - return Ok(new { success = true, data = result }); + return SuccessResponse(result); } catch (ArgumentException ex) { _logger.LogWarning(ex, "Invalid request for flashcard {FlashcardId}", flashcardId); - return BadRequest(new { success = false, error = ex.Message }); + return ErrorResponse("INVALID_REQUEST", ex.Message, null, 400); } catch (Exception ex) { _logger.LogError(ex, "Failed to start image generation for flashcard {FlashcardId}", flashcardId); - return StatusCode(500, new { success = false, error = "Failed to start generation" }); + return ErrorResponse("GENERATION_FAILED", "Failed to start generation"); } } @@ -73,17 +70,17 @@ public class ImageGenerationController : ControllerBase var status = await _orchestrator.GetGenerationStatusAsync(requestId); - return Ok(new { success = true, data = status }); + return SuccessResponse(status); } catch (ArgumentException ex) { _logger.LogWarning(ex, "Generation request {RequestId} not found", requestId); - return NotFound(new { success = false, error = ex.Message }); + return ErrorResponse("NOT_FOUND", ex.Message, null, 404); } catch (Exception ex) { _logger.LogError(ex, "Failed to get status for request {RequestId}", requestId); - return StatusCode(500, new { success = false, error = "Failed to get status" }); + return ErrorResponse("STATUS_ERROR", "Failed to get status"); } } @@ -105,17 +102,17 @@ public class ImageGenerationController : ControllerBase if (cancelled) { - return Ok(new { success = true, message = "Generation cancelled successfully" }); + return SuccessResponse(new { message = "Generation cancelled successfully" }); } else { - return BadRequest(new { success = false, error = "Cannot cancel this request" }); + return ErrorResponse("CANCEL_FAILED", "Cannot cancel this request", null, 400); } } catch (Exception ex) { _logger.LogError(ex, "Failed to cancel generation request {RequestId}", requestId); - return StatusCode(500, new { success = false, error = "Failed to cancel generation" }); + return ErrorResponse("CANCEL_ERROR", "Failed to cancel generation"); } } @@ -148,12 +145,12 @@ public class ImageGenerationController : ControllerBase } }; - return Ok(new { success = true, data = history }); + return SuccessResponse(history); } catch (Exception ex) { _logger.LogError(ex, "Failed to get generation history for user"); - return StatusCode(500, new { success = false, error = "Failed to get history" }); + return ErrorResponse("HISTORY_ERROR", "Failed to get history"); } } diff --git a/backend/DramaLing.Api/Controllers/OptionsVocabularyTestController.cs b/backend/DramaLing.Api/Controllers/OptionsVocabularyTestController.cs index 07eeacc..7e793a2 100644 --- a/backend/DramaLing.Api/Controllers/OptionsVocabularyTestController.cs +++ b/backend/DramaLing.Api/Controllers/OptionsVocabularyTestController.cs @@ -6,19 +6,16 @@ namespace DramaLing.Api.Controllers; /// /// 選項詞彙庫服務測試控制器 (僅用於開發測試) /// -[ApiController] [Route("api/test/[controller]")] -public class OptionsVocabularyTestController : ControllerBase +public class OptionsVocabularyTestController : BaseController { private readonly IOptionsVocabularyService _optionsVocabularyService; - private readonly ILogger _logger; public OptionsVocabularyTestController( IOptionsVocabularyService optionsVocabularyService, - ILogger logger) + ILogger logger) : base(logger) { _optionsVocabularyService = optionsVocabularyService; - _logger = logger; } /// diff --git a/backend/DramaLing.Api/Controllers/StatsController.cs b/backend/DramaLing.Api/Controllers/StatsController.cs index b5a6dc3..9cb7445 100644 --- a/backend/DramaLing.Api/Controllers/StatsController.cs +++ b/backend/DramaLing.Api/Controllers/StatsController.cs @@ -6,14 +6,13 @@ using System.Security.Claims; namespace DramaLing.Api.Controllers; -[ApiController] [Route("api/[controller]")] [Authorize] -public class StatsController : ControllerBase +public class StatsController : BaseController { private readonly DramaLingDbContext _context; - public StatsController(DramaLingDbContext context) + public StatsController(DramaLingDbContext context, ILogger logger) : base(logger) { _context = context; } diff --git a/backend/DramaLing.Api/Data/DramaLingDbContext.cs b/backend/DramaLing.Api/Data/DramaLingDbContext.cs index e55740a..f91ec61 100644 --- a/backend/DramaLing.Api/Data/DramaLingDbContext.cs +++ b/backend/DramaLing.Api/Data/DramaLingDbContext.cs @@ -49,14 +49,19 @@ public class DramaLingDbContext : DbContext modelBuilder.Entity().ToTable("flashcard_example_images"); modelBuilder.Entity().ToTable("image_generation_requests"); modelBuilder.Entity().ToTable("options_vocabularies"); + modelBuilder.Entity().ToTable("sentence_analysis_cache"); + modelBuilder.Entity().ToTable("word_query_usage_stats"); // 配置屬性名稱 (snake_case) ConfigureUserEntity(modelBuilder); + ConfigureUserSettingsEntity(modelBuilder); ConfigureFlashcardEntity(modelBuilder); // ConfigureStudyEntities 已移除 - StudyRecord 實體已清理 ConfigureTagEntities(modelBuilder); ConfigureErrorReportEntity(modelBuilder); ConfigureDailyStatsEntity(modelBuilder); + ConfigureSentenceAnalysisCacheEntity(modelBuilder); + ConfigureWordQueryUsageStatsEntity(modelBuilder); ConfigureAudioEntities(modelBuilder); ConfigureImageGenerationEntities(modelBuilder); ConfigureOptionsVocabularyEntity(modelBuilder); @@ -79,6 +84,7 @@ public class DramaLingDbContext : DbContext private void ConfigureUserEntity(ModelBuilder modelBuilder) { var userEntity = modelBuilder.Entity(); + userEntity.Property(u => u.Id).HasColumnName("id"); userEntity.Property(u => u.Username).HasColumnName("username"); userEntity.Property(u => u.Email).HasColumnName("email"); userEntity.Property(u => u.PasswordHash).HasColumnName("password_hash"); @@ -108,8 +114,14 @@ public class DramaLingDbContext : DbContext private void ConfigureFlashcardEntity(ModelBuilder modelBuilder) { var flashcardEntity = modelBuilder.Entity(); + flashcardEntity.Property(f => f.Id).HasColumnName("id"); flashcardEntity.Property(f => f.UserId).HasColumnName("user_id"); + flashcardEntity.Property(f => f.Word).HasColumnName("word"); + flashcardEntity.Property(f => f.Translation).HasColumnName("translation"); + flashcardEntity.Property(f => f.Definition).HasColumnName("definition"); flashcardEntity.Property(f => f.PartOfSpeech).HasColumnName("part_of_speech"); + flashcardEntity.Property(f => f.Pronunciation).HasColumnName("pronunciation"); + flashcardEntity.Property(f => f.Example).HasColumnName("example"); flashcardEntity.Property(f => f.ExampleTranslation).HasColumnName("example_translation"); // 已刪除的復習相關屬性配置 // EasinessFactor, IntervalDays, NextReviewDate, MasteryLevel, @@ -126,7 +138,10 @@ public class DramaLingDbContext : DbContext private void ConfigureTagEntities(ModelBuilder modelBuilder) { var tagEntity = modelBuilder.Entity(); + tagEntity.Property(t => t.Id).HasColumnName("id"); tagEntity.Property(t => t.UserId).HasColumnName("user_id"); + tagEntity.Property(t => t.Name).HasColumnName("name"); + tagEntity.Property(t => t.Color).HasColumnName("color"); tagEntity.Property(t => t.UsageCount).HasColumnName("usage_count"); tagEntity.Property(t => t.CreatedAt).HasColumnName("created_at"); @@ -138,10 +153,13 @@ public class DramaLingDbContext : DbContext private void ConfigureErrorReportEntity(ModelBuilder modelBuilder) { var errorEntity = modelBuilder.Entity(); + errorEntity.Property(e => e.Id).HasColumnName("id"); errorEntity.Property(e => e.UserId).HasColumnName("user_id"); errorEntity.Property(e => e.FlashcardId).HasColumnName("flashcard_id"); errorEntity.Property(e => e.ReportType).HasColumnName("report_type"); + errorEntity.Property(e => e.Description).HasColumnName("description"); errorEntity.Property(e => e.StudyMode).HasColumnName("study_mode"); + errorEntity.Property(e => e.Status).HasColumnName("status"); errorEntity.Property(e => e.AdminNotes).HasColumnName("admin_notes"); errorEntity.Property(e => e.ResolvedAt).HasColumnName("resolved_at"); errorEntity.Property(e => e.ResolvedBy).HasColumnName("resolved_by"); @@ -151,7 +169,9 @@ public class DramaLingDbContext : DbContext private void ConfigureDailyStatsEntity(ModelBuilder modelBuilder) { var statsEntity = modelBuilder.Entity(); + statsEntity.Property(d => d.Id).HasColumnName("id"); statsEntity.Property(d => d.UserId).HasColumnName("user_id"); + statsEntity.Property(d => d.Date).HasColumnName("date"); statsEntity.Property(d => d.WordsStudied).HasColumnName("words_studied"); statsEntity.Property(d => d.WordsCorrect).HasColumnName("words_correct"); statsEntity.Property(d => d.StudyTimeSeconds).HasColumnName("study_time_seconds"); @@ -161,6 +181,54 @@ public class DramaLingDbContext : DbContext statsEntity.Property(d => d.CreatedAt).HasColumnName("created_at"); } + private void ConfigureUserSettingsEntity(ModelBuilder modelBuilder) + { + var settingsEntity = modelBuilder.Entity(); + settingsEntity.Property(us => us.Id).HasColumnName("id"); + settingsEntity.Property(us => us.UserId).HasColumnName("user_id"); + settingsEntity.Property(us => us.DailyGoal).HasColumnName("daily_goal"); + settingsEntity.Property(us => us.ReminderTime).HasColumnName("reminder_time"); + settingsEntity.Property(us => us.ReminderEnabled).HasColumnName("reminder_enabled"); + settingsEntity.Property(us => us.DifficultyPreference).HasColumnName("difficulty_preference"); + settingsEntity.Property(us => us.AutoPlayAudio).HasColumnName("auto_play_audio"); + settingsEntity.Property(us => us.ShowPronunciation).HasColumnName("show_pronunciation"); + settingsEntity.Property(us => us.CreatedAt).HasColumnName("created_at"); + settingsEntity.Property(us => us.UpdatedAt).HasColumnName("updated_at"); + } + + private void ConfigureSentenceAnalysisCacheEntity(ModelBuilder modelBuilder) + { + var cacheEntity = modelBuilder.Entity(); + cacheEntity.Property(sac => sac.Id).HasColumnName("id"); + cacheEntity.Property(sac => sac.InputTextHash).HasColumnName("input_text_hash"); + cacheEntity.Property(sac => sac.InputText).HasColumnName("input_text"); + cacheEntity.Property(sac => sac.CorrectedText).HasColumnName("corrected_text"); + cacheEntity.Property(sac => sac.HasGrammarErrors).HasColumnName("has_grammar_errors"); + cacheEntity.Property(sac => sac.GrammarCorrections).HasColumnName("grammar_corrections"); + cacheEntity.Property(sac => sac.AnalysisResult).HasColumnName("analysis_result"); + cacheEntity.Property(sac => sac.HighValueWords).HasColumnName("high_value_words"); + cacheEntity.Property(sac => sac.IdiomsDetected).HasColumnName("idioms_detected"); + cacheEntity.Property(sac => sac.CreatedAt).HasColumnName("created_at"); + cacheEntity.Property(sac => sac.ExpiresAt).HasColumnName("expires_at"); + cacheEntity.Property(sac => sac.AccessCount).HasColumnName("access_count"); + cacheEntity.Property(sac => sac.LastAccessedAt).HasColumnName("last_accessed_at"); + } + + private void ConfigureWordQueryUsageStatsEntity(ModelBuilder modelBuilder) + { + var statsEntity = modelBuilder.Entity(); + statsEntity.Property(wq => wq.Id).HasColumnName("id"); + statsEntity.Property(wq => wq.UserId).HasColumnName("user_id"); + statsEntity.Property(wq => wq.Date).HasColumnName("date"); + statsEntity.Property(wq => wq.SentenceAnalysisCount).HasColumnName("sentence_analysis_count"); + statsEntity.Property(wq => wq.HighValueWordClicks).HasColumnName("high_value_word_clicks"); + statsEntity.Property(wq => wq.LowValueWordClicks).HasColumnName("low_value_word_clicks"); + statsEntity.Property(wq => wq.TotalApiCalls).HasColumnName("total_api_calls"); + statsEntity.Property(wq => wq.UniqueWordsQueried).HasColumnName("unique_words_queried"); + statsEntity.Property(wq => wq.CreatedAt).HasColumnName("created_at"); + statsEntity.Property(wq => wq.UpdatedAt).HasColumnName("updated_at"); + } + private void ConfigureRelationships(ModelBuilder modelBuilder) { // User relationships @@ -255,8 +323,10 @@ public class DramaLingDbContext : DbContext { // AudioCache configuration var audioCacheEntity = modelBuilder.Entity(); + audioCacheEntity.Property(ac => ac.Id).HasColumnName("id"); audioCacheEntity.Property(ac => ac.TextHash).HasColumnName("text_hash"); audioCacheEntity.Property(ac => ac.TextContent).HasColumnName("text_content"); + audioCacheEntity.Property(ac => ac.Accent).HasColumnName("accent"); audioCacheEntity.Property(ac => ac.VoiceId).HasColumnName("voice_id"); audioCacheEntity.Property(ac => ac.AudioUrl).HasColumnName("audio_url"); audioCacheEntity.Property(ac => ac.FileSize).HasColumnName("file_size"); @@ -274,6 +344,7 @@ public class DramaLingDbContext : DbContext // PronunciationAssessment configuration var pronunciationEntity = modelBuilder.Entity(); + pronunciationEntity.Property(pa => pa.Id).HasColumnName("id"); pronunciationEntity.Property(pa => pa.UserId).HasColumnName("user_id"); pronunciationEntity.Property(pa => pa.FlashcardId).HasColumnName("flashcard_id"); pronunciationEntity.Property(pa => pa.TargetText).HasColumnName("target_text"); @@ -296,6 +367,7 @@ public class DramaLingDbContext : DbContext // UserAudioPreferences configuration var audioPrefsEntity = modelBuilder.Entity(); + audioPrefsEntity.Property(uap => uap.UserId).HasColumnName("user_id"); audioPrefsEntity.Property(uap => uap.PreferredAccent).HasColumnName("preferred_accent"); audioPrefsEntity.Property(uap => uap.PreferredVoiceMale).HasColumnName("preferred_voice_male"); audioPrefsEntity.Property(uap => uap.PreferredVoiceFemale).HasColumnName("preferred_voice_female"); @@ -336,6 +408,7 @@ public class DramaLingDbContext : DbContext { // ExampleImage configuration var exampleImageEntity = modelBuilder.Entity(); + exampleImageEntity.Property(ei => ei.Id).HasColumnName("id"); exampleImageEntity.Property(ei => ei.RelativePath).HasColumnName("relative_path"); exampleImageEntity.Property(ei => ei.AltText).HasColumnName("alt_text"); exampleImageEntity.Property(ei => ei.GeminiPrompt).HasColumnName("gemini_prompt"); @@ -371,6 +444,7 @@ public class DramaLingDbContext : DbContext // ImageGenerationRequest configuration var generationRequestEntity = modelBuilder.Entity(); + generationRequestEntity.Property(igr => igr.Id).HasColumnName("id"); generationRequestEntity.Property(igr => igr.UserId).HasColumnName("user_id"); generationRequestEntity.Property(igr => igr.FlashcardId).HasColumnName("flashcard_id"); generationRequestEntity.Property(igr => igr.OverallStatus).HasColumnName("overall_status"); @@ -433,6 +507,8 @@ public class DramaLingDbContext : DbContext var optionsVocabEntity = modelBuilder.Entity(); // Configure column names (snake_case) + optionsVocabEntity.Property(ov => ov.Id).HasColumnName("id"); + optionsVocabEntity.Property(ov => ov.Word).HasColumnName("word"); optionsVocabEntity.Property(ov => ov.CEFRLevel).HasColumnName("cefr_level"); optionsVocabEntity.Property(ov => ov.PartOfSpeech).HasColumnName("part_of_speech"); optionsVocabEntity.Property(ov => ov.WordLength).HasColumnName("word_length"); diff --git a/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs b/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs index 7fa62b5..1dbb05b 100644 --- a/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs +++ b/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs @@ -69,27 +69,35 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddScoped(); - // 快取提供者 - services.AddScoped(provider => - new MemoryCacheProvider( - provider.GetRequiredService(), - provider.GetRequiredService>())); - - services.AddScoped(provider => + // 快取提供者 - 使用具名註冊 + services.AddScoped(provider => { + var memoryCache = provider.GetRequiredService(); + var logger = provider.GetRequiredService>(); + var databaseCacheManager = provider.GetRequiredService(); + var strategyManager = provider.GetRequiredService(); + + var memoryProvider = new MemoryCacheProvider( + memoryCache, + provider.GetRequiredService>()); + + ICacheProvider? distributedProvider = null; var distributedCache = provider.GetService(); if (distributedCache != null) { - return new DistributedCacheProvider( + distributedProvider = new DistributedCacheProvider( distributedCache, provider.GetRequiredService(), provider.GetRequiredService>()); } - return null!; - }); - // 主要快取服務 - services.AddScoped(); + return new HybridCacheService( + memoryProvider, + distributedProvider, + databaseCacheManager, + strategyManager, + logger); + }); return services; } diff --git a/backend/DramaLing.Api/Migrations/20250930081940_FixFlashcardColumnNaming.Designer.cs b/backend/DramaLing.Api/Migrations/20250930081940_FixFlashcardColumnNaming.Designer.cs new file mode 100644 index 0000000..02a0fad --- /dev/null +++ b/backend/DramaLing.Api/Migrations/20250930081940_FixFlashcardColumnNaming.Designer.cs @@ -0,0 +1,1201 @@ +// +using System; +using DramaLing.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DramaLing.Api.Migrations +{ + [DbContext(typeof(DramaLingDbContext))] + [Migration("20250930081940_FixFlashcardColumnNaming")] + partial class FixFlashcardColumnNaming + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.AudioCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Accent") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AudioUrl") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("audio_url"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DurationMs") + .HasColumnType("INTEGER") + .HasColumnName("duration_ms"); + + b.Property("FileSize") + .HasColumnType("INTEGER") + .HasColumnName("file_size"); + + b.Property("LastAccessed") + .HasColumnType("TEXT") + .HasColumnName("last_accessed"); + + b.Property("TextContent") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("text_content"); + + b.Property("TextHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("text_hash"); + + b.Property("VoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("voice_id"); + + b.HasKey("Id"); + + b.HasIndex("LastAccessed") + .HasDatabaseName("IX_AudioCache_LastAccessed"); + + b.HasIndex("TextHash") + .IsUnique() + .HasDatabaseName("IX_AudioCache_TextHash"); + + b.ToTable("audio_cache", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AiApiCalls") + .HasColumnType("INTEGER") + .HasColumnName("ai_api_calls"); + + b.Property("CardsGenerated") + .HasColumnType("INTEGER") + .HasColumnName("cards_generated"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("SessionCount") + .HasColumnType("INTEGER") + .HasColumnName("session_count"); + + b.Property("StudyTimeSeconds") + .HasColumnType("INTEGER") + .HasColumnName("study_time_seconds"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("WordsCorrect") + .HasColumnType("INTEGER") + .HasColumnName("words_correct"); + + b.Property("WordsStudied") + .HasColumnType("INTEGER") + .HasColumnName("words_studied"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Date") + .IsUnique(); + + b.ToTable("daily_stats", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AdminNotes") + .HasColumnType("TEXT") + .HasColumnName("admin_notes"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("ReportType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("report_type"); + + b.Property("ResolvedAt") + .HasColumnType("TEXT") + .HasColumnName("resolved_at"); + + b.Property("ResolvedBy") + .HasColumnType("TEXT") + .HasColumnName("resolved_by"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StudyMode") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("study_mode"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("ResolvedBy"); + + b.HasIndex("UserId"); + + b.ToTable("error_reports", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AltText") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("alt_text"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("content_hash"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FileSize") + .HasColumnType("INTEGER") + .HasColumnName("file_size"); + + b.Property("GeminiCost") + .HasColumnType("TEXT") + .HasColumnName("gemini_cost"); + + b.Property("GeminiDescription") + .HasColumnType("TEXT") + .HasColumnName("gemini_description"); + + b.Property("GeminiPrompt") + .HasColumnType("TEXT") + .HasColumnName("gemini_prompt"); + + b.Property("ImageHeight") + .HasColumnType("INTEGER") + .HasColumnName("image_height"); + + b.Property("ImageWidth") + .HasColumnType("INTEGER") + .HasColumnName("image_width"); + + b.Property("ModerationNotes") + .HasColumnType("TEXT") + .HasColumnName("moderation_notes"); + + b.Property("ModerationStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("moderation_status"); + + b.Property("QualityScore") + .HasColumnType("TEXT") + .HasColumnName("quality_score"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("relative_path"); + + b.Property("ReplicateCost") + .HasColumnType("TEXT") + .HasColumnName("replicate_cost"); + + b.Property("ReplicateModel") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("replicate_model"); + + b.Property("ReplicatePrompt") + .HasColumnType("TEXT") + .HasColumnName("replicate_prompt"); + + b.Property("ReplicateVersion") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("replicate_version"); + + b.Property("TotalGenerationCost") + .HasColumnType("TEXT") + .HasColumnName("total_generation_cost"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("AccessCount"); + + b.HasIndex("ContentHash") + .IsUnique(); + + b.ToTable("example_images", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Definition") + .HasColumnType("TEXT") + .HasColumnName("definition"); + + b.Property("DifficultyLevel") + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("difficulty_level"); + + b.Property("Example") + .HasColumnType("TEXT") + .HasColumnName("example"); + + b.Property("ExampleTranslation") + .HasColumnType("TEXT") + .HasColumnName("example_translation"); + + b.Property("IsArchived") + .HasColumnType("INTEGER") + .HasColumnName("is_archived"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER") + .HasColumnName("is_favorite"); + + b.Property("PartOfSpeech") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("part_of_speech"); + + b.Property("Pronunciation") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("pronunciation"); + + b.Property("Translation") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("translation"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("word"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("flashcards", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b => + { + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("ExampleImageId") + .HasColumnType("TEXT") + .HasColumnName("example_image_id"); + + b.Property("ContextRelevance") + .HasColumnType("TEXT") + .HasColumnName("context_relevance"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER") + .HasColumnName("display_order"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER") + .HasColumnName("is_primary"); + + b.HasKey("FlashcardId", "ExampleImageId"); + + b.HasIndex("ExampleImageId"); + + b.ToTable("flashcard_example_images", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b => + { + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("TagId") + .HasColumnType("TEXT") + .HasColumnName("tag_id"); + + b.HasKey("FlashcardId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("flashcard_tags", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FinalReplicatePrompt") + .HasColumnType("TEXT") + .HasColumnName("final_replicate_prompt"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("GeminiCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("gemini_completed_at"); + + b.Property("GeminiCost") + .HasColumnType("TEXT") + .HasColumnName("gemini_cost"); + + b.Property("GeminiErrorMessage") + .HasColumnType("TEXT") + .HasColumnName("gemini_error_message"); + + b.Property("GeminiProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("gemini_processing_time_ms"); + + b.Property("GeminiPrompt") + .HasColumnType("TEXT") + .HasColumnName("gemini_prompt"); + + b.Property("GeminiStartedAt") + .HasColumnType("TEXT") + .HasColumnName("gemini_started_at"); + + b.Property("GeminiStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("gemini_status"); + + b.Property("GeneratedDescription") + .HasColumnType("TEXT") + .HasColumnName("generated_description"); + + b.Property("GeneratedImageId") + .HasColumnType("TEXT") + .HasColumnName("generated_image_id"); + + b.Property("OriginalRequest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("original_request"); + + b.Property("OverallStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("overall_status"); + + b.Property("ReplicateCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("replicate_completed_at"); + + b.Property("ReplicateCost") + .HasColumnType("TEXT") + .HasColumnName("replicate_cost"); + + b.Property("ReplicateErrorMessage") + .HasColumnType("TEXT") + .HasColumnName("replicate_error_message"); + + b.Property("ReplicateProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("replicate_processing_time_ms"); + + b.Property("ReplicateStartedAt") + .HasColumnType("TEXT") + .HasColumnName("replicate_started_at"); + + b.Property("ReplicateStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("replicate_status"); + + b.Property("TotalCost") + .HasColumnType("TEXT") + .HasColumnName("total_cost"); + + b.Property("TotalProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("total_processing_time_ms"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("GeneratedImageId"); + + b.HasIndex("UserId"); + + b.ToTable("image_generation_requests", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.OptionsVocabulary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CEFRLevel") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("cefr_level"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("PartOfSpeech") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("part_of_speech"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("updated_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("WordLength") + .HasColumnType("INTEGER") + .HasColumnName("word_length"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsActive" }, "IX_OptionsVocabulary_Active"); + + b.HasIndex(new[] { "CEFRLevel" }, "IX_OptionsVocabulary_CEFR"); + + b.HasIndex(new[] { "CEFRLevel", "PartOfSpeech", "WordLength" }, "IX_OptionsVocabulary_Core_Matching"); + + b.HasIndex(new[] { "PartOfSpeech" }, "IX_OptionsVocabulary_PartOfSpeech"); + + b.HasIndex(new[] { "Word" }, "IX_OptionsVocabulary_Word") + .IsUnique(); + + b.HasIndex(new[] { "WordLength" }, "IX_OptionsVocabulary_WordLength"); + + b.ToTable("options_vocabularies", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccuracyScore") + .HasColumnType("TEXT") + .HasColumnName("accuracy_score"); + + b.Property("AudioUrl") + .HasColumnType("TEXT") + .HasColumnName("audio_url"); + + b.Property("CompletenessScore") + .HasColumnType("TEXT") + .HasColumnName("completeness_score"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("FluencyScore") + .HasColumnType("TEXT") + .HasColumnName("fluency_score"); + + b.Property("OverallScore") + .HasColumnType("INTEGER") + .HasColumnName("overall_score"); + + b.Property("PhonemeScores") + .HasColumnType("TEXT") + .HasColumnName("phoneme_scores"); + + b.Property("PracticeMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("practice_mode"); + + b.Property("ProsodyScore") + .HasColumnType("TEXT") + .HasColumnName("prosody_score"); + + b.Property("Suggestions") + .HasColumnType("TEXT") + .HasColumnName("suggestions"); + + b.Property("TargetText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("target_text"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("UserId", "FlashcardId") + .HasDatabaseName("IX_PronunciationAssessment_UserFlashcard"); + + b.ToTable("pronunciation_assessments", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.SentenceAnalysisCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("AnalysisResult") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CorrectedText") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("GrammarCorrections") + .HasColumnType("TEXT"); + + b.Property("HasGrammarErrors") + .HasColumnType("INTEGER"); + + b.Property("HighValueWords") + .HasColumnType("TEXT"); + + b.Property("IdiomsDetected") + .HasColumnType("TEXT"); + + b.Property("InputText") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("InputTextHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("LastAccessedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt") + .HasDatabaseName("IX_SentenceAnalysisCache_Expires"); + + b.HasIndex("InputTextHash") + .HasDatabaseName("IX_SentenceAnalysisCache_Hash"); + + b.HasIndex("InputTextHash", "ExpiresAt") + .HasDatabaseName("IX_SentenceAnalysisCache_Hash_Expires"); + + b.ToTable("SentenceAnalysisCache"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UsageCount") + .HasColumnType("INTEGER") + .HasColumnName("usage_count"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("tags", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AvatarUrl") + .HasColumnType("TEXT") + .HasColumnName("avatar_url"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DisplayName") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("display_name"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("email"); + + b.Property("EnglishLevel") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("english_level"); + + b.Property("IsLevelVerified") + .HasColumnType("INTEGER") + .HasColumnName("is_level_verified"); + + b.Property("LevelNotes") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("level_notes"); + + b.Property("LevelUpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("level_updated_at"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("password_hash"); + + b.Property("Preferences") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("preferences"); + + b.Property("SubscriptionType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("subscription_type"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("user_profiles", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("AutoPlayEnabled") + .HasColumnType("INTEGER") + .HasColumnName("auto_play_enabled"); + + b.Property("DefaultSpeed") + .HasColumnType("TEXT") + .HasColumnName("default_speed"); + + b.Property("EnableDetailedFeedback") + .HasColumnType("INTEGER") + .HasColumnName("enable_detailed_feedback"); + + b.Property("PreferredAccent") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("preferred_accent"); + + b.Property("PreferredVoiceFemale") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("preferred_voice_female"); + + b.Property("PreferredVoiceMale") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("preferred_voice_male"); + + b.Property("PronunciationDifficulty") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("pronunciation_difficulty"); + + b.Property("TargetScoreThreshold") + .HasColumnType("INTEGER") + .HasColumnName("target_score_threshold"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("UserId"); + + b.ToTable("user_audio_preferences", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AutoPlayAudio") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DailyGoal") + .HasColumnType("INTEGER"); + + b.Property("DifficultyPreference") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("ReminderEnabled") + .HasColumnType("INTEGER"); + + b.Property("ReminderTime") + .HasColumnType("TEXT"); + + b.Property("ShowPronunciation") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_settings", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("HighValueWordClicks") + .HasColumnType("INTEGER"); + + b.Property("LowValueWordClicks") + .HasColumnType("INTEGER"); + + b.Property("SentenceAnalysisCount") + .HasColumnType("INTEGER"); + + b.Property("TotalApiCalls") + .HasColumnType("INTEGER"); + + b.Property("UniqueWordsQueried") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_WordQueryUsageStats_CreatedAt"); + + b.HasIndex("UserId", "Date") + .IsUnique() + .HasDatabaseName("IX_WordQueryUsageStats_UserDate"); + + b.ToTable("WordQueryUsageStats"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("DailyStats") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("ErrorReports") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.User", "ResolvedByUser") + .WithMany() + .HasForeignKey("ResolvedBy") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("ErrorReports") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("ResolvedByUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("Flashcards") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b => + { + b.HasOne("DramaLing.Api.Models.Entities.ExampleImage", "ExampleImage") + .WithMany("FlashcardExampleImages") + .HasForeignKey("ExampleImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("FlashcardExampleImages") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExampleImage"); + + b.Navigation("Flashcard"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("FlashcardTags") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.Tag", "Tag") + .WithMany("FlashcardTags") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.ExampleImage", "GeneratedImage") + .WithMany() + .HasForeignKey("GeneratedImageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("GeneratedImage"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithOne() + .HasForeignKey("DramaLing.Api.Models.Entities.UserAudioPreferences", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithOne("Settings") + .HasForeignKey("DramaLing.Api.Models.Entities.UserSettings", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => + { + b.Navigation("FlashcardExampleImages"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.Navigation("ErrorReports"); + + b.Navigation("FlashcardExampleImages"); + + b.Navigation("FlashcardTags"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.Navigation("FlashcardTags"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => + { + b.Navigation("DailyStats"); + + b.Navigation("ErrorReports"); + + b.Navigation("Flashcards"); + + b.Navigation("Settings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/DramaLing.Api/Migrations/20250930081940_FixFlashcardColumnNaming.cs b/backend/DramaLing.Api/Migrations/20250930081940_FixFlashcardColumnNaming.cs new file mode 100644 index 0000000..5857e00 --- /dev/null +++ b/backend/DramaLing.Api/Migrations/20250930081940_FixFlashcardColumnNaming.cs @@ -0,0 +1,428 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DramaLing.Api.Migrations +{ + /// + public partial class FixFlashcardColumnNaming : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_pronunciation_assessments_study_sessions_study_session_id", + table: "pronunciation_assessments"); + + migrationBuilder.DropTable( + name: "study_records"); + + migrationBuilder.DropTable( + name: "test_results"); + + migrationBuilder.DropTable( + name: "study_cards"); + + migrationBuilder.DropTable( + name: "study_sessions"); + + migrationBuilder.DropIndex( + name: "IX_PronunciationAssessment_Session", + table: "pronunciation_assessments"); + + migrationBuilder.DropColumn( + name: "study_session_id", + table: "pronunciation_assessments"); + + migrationBuilder.DropColumn( + name: "FilledQuestionText", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "LastQuestionType", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "Repetitions", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "ReviewHistory", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "Synonyms", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "easiness_factor", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "interval_days", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "last_reviewed_at", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "mastery_level", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "next_review_date", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "times_correct", + table: "flashcards"); + + migrationBuilder.DropColumn( + name: "times_reviewed", + table: "flashcards"); + + migrationBuilder.RenameColumn( + name: "Word", + table: "flashcards", + newName: "word"); + + migrationBuilder.RenameColumn( + name: "Translation", + table: "flashcards", + newName: "translation"); + + migrationBuilder.RenameColumn( + name: "Pronunciation", + table: "flashcards", + newName: "pronunciation"); + + migrationBuilder.RenameColumn( + name: "Example", + table: "flashcards", + newName: "example"); + + migrationBuilder.RenameColumn( + name: "Definition", + table: "flashcards", + newName: "definition"); + + migrationBuilder.AlterColumn( + name: "definition", + table: "flashcards", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "word", + table: "flashcards", + newName: "Word"); + + migrationBuilder.RenameColumn( + name: "translation", + table: "flashcards", + newName: "Translation"); + + migrationBuilder.RenameColumn( + name: "pronunciation", + table: "flashcards", + newName: "Pronunciation"); + + migrationBuilder.RenameColumn( + name: "example", + table: "flashcards", + newName: "Example"); + + migrationBuilder.RenameColumn( + name: "definition", + table: "flashcards", + newName: "Definition"); + + migrationBuilder.AddColumn( + name: "study_session_id", + table: "pronunciation_assessments", + type: "TEXT", + nullable: true); + + migrationBuilder.AlterColumn( + name: "Definition", + table: "flashcards", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "FilledQuestionText", + table: "flashcards", + type: "TEXT", + maxLength: 1000, + nullable: true); + + migrationBuilder.AddColumn( + name: "LastQuestionType", + table: "flashcards", + type: "TEXT", + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "Repetitions", + table: "flashcards", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ReviewHistory", + table: "flashcards", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "Synonyms", + table: "flashcards", + type: "TEXT", + maxLength: 2000, + nullable: true); + + migrationBuilder.AddColumn( + name: "easiness_factor", + table: "flashcards", + type: "REAL", + nullable: false, + defaultValue: 0f); + + migrationBuilder.AddColumn( + name: "interval_days", + table: "flashcards", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "last_reviewed_at", + table: "flashcards", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "mastery_level", + table: "flashcards", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "next_review_date", + table: "flashcards", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "times_correct", + table: "flashcards", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "times_reviewed", + table: "flashcards", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "study_sessions", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + user_id = table.Column(type: "TEXT", nullable: false), + average_response_time_ms = table.Column(type: "INTEGER", nullable: false), + CompletedCards = table.Column(type: "INTEGER", nullable: false), + CompletedTests = table.Column(type: "INTEGER", nullable: false), + correct_count = table.Column(type: "INTEGER", nullable: false), + CurrentCardIndex = table.Column(type: "INTEGER", nullable: false), + CurrentTestType = table.Column(type: "TEXT", maxLength: 50, nullable: true), + duration_seconds = table.Column(type: "INTEGER", nullable: false), + ended_at = table.Column(type: "TEXT", nullable: true), + session_type = table.Column(type: "TEXT", maxLength: 50, nullable: false), + started_at = table.Column(type: "TEXT", nullable: false), + Status = table.Column(type: "INTEGER", nullable: false), + total_cards = table.Column(type: "INTEGER", nullable: false), + TotalTests = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_study_sessions", x => x.Id); + table.ForeignKey( + name: "FK_study_sessions_user_profiles_user_id", + column: x => x.user_id, + principalTable: "user_profiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "study_cards", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + FlashcardId = table.Column(type: "TEXT", nullable: false), + StudySessionId = table.Column(type: "TEXT", nullable: false), + CompletedAt = table.Column(type: "TEXT", nullable: true), + IsCompleted = table.Column(type: "INTEGER", nullable: false), + Order = table.Column(type: "INTEGER", nullable: false), + PlannedTests = table.Column(type: "TEXT", nullable: false), + PlannedTestsJson = table.Column(type: "TEXT", nullable: false), + StartedAt = table.Column(type: "TEXT", nullable: false), + Word = table.Column(type: "TEXT", maxLength: 100, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_study_cards", x => x.Id); + table.ForeignKey( + name: "FK_study_cards_flashcards_FlashcardId", + column: x => x.FlashcardId, + principalTable: "flashcards", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_study_cards_study_sessions_StudySessionId", + column: x => x.StudySessionId, + principalTable: "study_sessions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "study_records", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + flashcard_id = table.Column(type: "TEXT", nullable: false), + session_id = table.Column(type: "TEXT", nullable: false), + user_id = table.Column(type: "TEXT", nullable: false), + is_correct = table.Column(type: "INTEGER", nullable: false), + NewEasinessFactor = table.Column(type: "REAL", nullable: false), + NewIntervalDays = table.Column(type: "INTEGER", nullable: false), + NewRepetitions = table.Column(type: "INTEGER", nullable: false), + NextReviewDate = table.Column(type: "TEXT", nullable: false), + PreviousEasinessFactor = table.Column(type: "REAL", nullable: false), + PreviousIntervalDays = table.Column(type: "INTEGER", nullable: false), + PreviousRepetitions = table.Column(type: "INTEGER", nullable: false), + quality_rating = table.Column(type: "INTEGER", nullable: false), + response_time_ms = table.Column(type: "INTEGER", nullable: true), + studied_at = table.Column(type: "TEXT", nullable: false), + study_mode = table.Column(type: "TEXT", maxLength: 50, nullable: false), + user_answer = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_study_records", x => x.Id); + table.ForeignKey( + name: "FK_study_records_flashcards_flashcard_id", + column: x => x.flashcard_id, + principalTable: "flashcards", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_study_records_study_sessions_session_id", + column: x => x.session_id, + principalTable: "study_sessions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_study_records_user_profiles_user_id", + column: x => x.user_id, + principalTable: "user_profiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "test_results", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + StudyCardId = table.Column(type: "TEXT", nullable: false), + CompletedAt = table.Column(type: "TEXT", nullable: false), + ConfidenceLevel = table.Column(type: "INTEGER", nullable: true), + IsCorrect = table.Column(type: "INTEGER", nullable: false), + ResponseTimeMs = table.Column(type: "INTEGER", nullable: false), + TestType = table.Column(type: "TEXT", maxLength: 50, nullable: false), + UserAnswer = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_test_results", x => x.Id); + table.ForeignKey( + name: "FK_test_results_study_cards_StudyCardId", + column: x => x.StudyCardId, + principalTable: "study_cards", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PronunciationAssessment_Session", + table: "pronunciation_assessments", + column: "study_session_id"); + + migrationBuilder.CreateIndex( + name: "IX_study_cards_FlashcardId", + table: "study_cards", + column: "FlashcardId"); + + migrationBuilder.CreateIndex( + name: "IX_study_cards_StudySessionId", + table: "study_cards", + column: "StudySessionId"); + + migrationBuilder.CreateIndex( + name: "IX_study_records_flashcard_id", + table: "study_records", + column: "flashcard_id"); + + migrationBuilder.CreateIndex( + name: "IX_study_records_session_id", + table: "study_records", + column: "session_id"); + + migrationBuilder.CreateIndex( + name: "IX_StudyRecord_UserCard_TestType_Unique", + table: "study_records", + columns: new[] { "user_id", "flashcard_id", "study_mode" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_study_sessions_user_id", + table: "study_sessions", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_test_results_StudyCardId", + table: "test_results", + column: "StudyCardId"); + + migrationBuilder.AddForeignKey( + name: "FK_pronunciation_assessments_study_sessions_study_session_id", + table: "pronunciation_assessments", + column: "study_session_id", + principalTable: "study_sessions", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + } +} diff --git a/backend/DramaLing.Api/Migrations/20250930083856_CompleteSnakeCaseNaming.Designer.cs b/backend/DramaLing.Api/Migrations/20250930083856_CompleteSnakeCaseNaming.Designer.cs new file mode 100644 index 0000000..70f6542 --- /dev/null +++ b/backend/DramaLing.Api/Migrations/20250930083856_CompleteSnakeCaseNaming.Designer.cs @@ -0,0 +1,1244 @@ +// +using System; +using DramaLing.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DramaLing.Api.Migrations +{ + [DbContext(typeof(DramaLingDbContext))] + [Migration("20250930083856_CompleteSnakeCaseNaming")] + partial class CompleteSnakeCaseNaming + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.AudioCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Accent") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AudioUrl") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("audio_url"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DurationMs") + .HasColumnType("INTEGER") + .HasColumnName("duration_ms"); + + b.Property("FileSize") + .HasColumnType("INTEGER") + .HasColumnName("file_size"); + + b.Property("LastAccessed") + .HasColumnType("TEXT") + .HasColumnName("last_accessed"); + + b.Property("TextContent") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("text_content"); + + b.Property("TextHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("text_hash"); + + b.Property("VoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("voice_id"); + + b.HasKey("Id"); + + b.HasIndex("LastAccessed") + .HasDatabaseName("IX_AudioCache_LastAccessed"); + + b.HasIndex("TextHash") + .IsUnique() + .HasDatabaseName("IX_AudioCache_TextHash"); + + b.ToTable("audio_cache", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AiApiCalls") + .HasColumnType("INTEGER") + .HasColumnName("ai_api_calls"); + + b.Property("CardsGenerated") + .HasColumnType("INTEGER") + .HasColumnName("cards_generated"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("SessionCount") + .HasColumnType("INTEGER") + .HasColumnName("session_count"); + + b.Property("StudyTimeSeconds") + .HasColumnType("INTEGER") + .HasColumnName("study_time_seconds"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("WordsCorrect") + .HasColumnType("INTEGER") + .HasColumnName("words_correct"); + + b.Property("WordsStudied") + .HasColumnType("INTEGER") + .HasColumnName("words_studied"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Date") + .IsUnique(); + + b.ToTable("daily_stats", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AdminNotes") + .HasColumnType("TEXT") + .HasColumnName("admin_notes"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("ReportType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("report_type"); + + b.Property("ResolvedAt") + .HasColumnType("TEXT") + .HasColumnName("resolved_at"); + + b.Property("ResolvedBy") + .HasColumnType("TEXT") + .HasColumnName("resolved_by"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("StudyMode") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("study_mode"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("ResolvedBy"); + + b.HasIndex("UserId"); + + b.ToTable("error_reports", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AltText") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("alt_text"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("content_hash"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FileSize") + .HasColumnType("INTEGER") + .HasColumnName("file_size"); + + b.Property("GeminiCost") + .HasColumnType("TEXT") + .HasColumnName("gemini_cost"); + + b.Property("GeminiDescription") + .HasColumnType("TEXT") + .HasColumnName("gemini_description"); + + b.Property("GeminiPrompt") + .HasColumnType("TEXT") + .HasColumnName("gemini_prompt"); + + b.Property("ImageHeight") + .HasColumnType("INTEGER") + .HasColumnName("image_height"); + + b.Property("ImageWidth") + .HasColumnType("INTEGER") + .HasColumnName("image_width"); + + b.Property("ModerationNotes") + .HasColumnType("TEXT") + .HasColumnName("moderation_notes"); + + b.Property("ModerationStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("moderation_status"); + + b.Property("QualityScore") + .HasColumnType("TEXT") + .HasColumnName("quality_score"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("relative_path"); + + b.Property("ReplicateCost") + .HasColumnType("TEXT") + .HasColumnName("replicate_cost"); + + b.Property("ReplicateModel") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("replicate_model"); + + b.Property("ReplicatePrompt") + .HasColumnType("TEXT") + .HasColumnName("replicate_prompt"); + + b.Property("ReplicateVersion") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("replicate_version"); + + b.Property("TotalGenerationCost") + .HasColumnType("TEXT") + .HasColumnName("total_generation_cost"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("AccessCount"); + + b.HasIndex("ContentHash") + .IsUnique(); + + b.ToTable("example_images", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Definition") + .HasColumnType("TEXT") + .HasColumnName("definition"); + + b.Property("DifficultyLevel") + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("difficulty_level"); + + b.Property("Example") + .HasColumnType("TEXT") + .HasColumnName("example"); + + b.Property("ExampleTranslation") + .HasColumnType("TEXT") + .HasColumnName("example_translation"); + + b.Property("IsArchived") + .HasColumnType("INTEGER") + .HasColumnName("is_archived"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER") + .HasColumnName("is_favorite"); + + b.Property("PartOfSpeech") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("part_of_speech"); + + b.Property("Pronunciation") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("pronunciation"); + + b.Property("Translation") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("translation"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("word"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("flashcards", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b => + { + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("ExampleImageId") + .HasColumnType("TEXT") + .HasColumnName("example_image_id"); + + b.Property("ContextRelevance") + .HasColumnType("TEXT") + .HasColumnName("context_relevance"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER") + .HasColumnName("display_order"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER") + .HasColumnName("is_primary"); + + b.HasKey("FlashcardId", "ExampleImageId"); + + b.HasIndex("ExampleImageId"); + + b.ToTable("flashcard_example_images", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b => + { + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("TagId") + .HasColumnType("TEXT") + .HasColumnName("tag_id"); + + b.HasKey("FlashcardId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("flashcard_tags", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FinalReplicatePrompt") + .HasColumnType("TEXT") + .HasColumnName("final_replicate_prompt"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("GeminiCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("gemini_completed_at"); + + b.Property("GeminiCost") + .HasColumnType("TEXT") + .HasColumnName("gemini_cost"); + + b.Property("GeminiErrorMessage") + .HasColumnType("TEXT") + .HasColumnName("gemini_error_message"); + + b.Property("GeminiProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("gemini_processing_time_ms"); + + b.Property("GeminiPrompt") + .HasColumnType("TEXT") + .HasColumnName("gemini_prompt"); + + b.Property("GeminiStartedAt") + .HasColumnType("TEXT") + .HasColumnName("gemini_started_at"); + + b.Property("GeminiStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("gemini_status"); + + b.Property("GeneratedDescription") + .HasColumnType("TEXT") + .HasColumnName("generated_description"); + + b.Property("GeneratedImageId") + .HasColumnType("TEXT") + .HasColumnName("generated_image_id"); + + b.Property("OriginalRequest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("original_request"); + + b.Property("OverallStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("overall_status"); + + b.Property("ReplicateCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("replicate_completed_at"); + + b.Property("ReplicateCost") + .HasColumnType("TEXT") + .HasColumnName("replicate_cost"); + + b.Property("ReplicateErrorMessage") + .HasColumnType("TEXT") + .HasColumnName("replicate_error_message"); + + b.Property("ReplicateProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("replicate_processing_time_ms"); + + b.Property("ReplicateStartedAt") + .HasColumnType("TEXT") + .HasColumnName("replicate_started_at"); + + b.Property("ReplicateStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("replicate_status"); + + b.Property("TotalCost") + .HasColumnType("TEXT") + .HasColumnName("total_cost"); + + b.Property("TotalProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("total_processing_time_ms"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("GeneratedImageId"); + + b.HasIndex("UserId"); + + b.ToTable("image_generation_requests", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.OptionsVocabulary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CEFRLevel") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("cefr_level"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("PartOfSpeech") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("part_of_speech"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("updated_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("WordLength") + .HasColumnType("INTEGER") + .HasColumnName("word_length"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsActive" }, "IX_OptionsVocabulary_Active"); + + b.HasIndex(new[] { "CEFRLevel" }, "IX_OptionsVocabulary_CEFR"); + + b.HasIndex(new[] { "CEFRLevel", "PartOfSpeech", "WordLength" }, "IX_OptionsVocabulary_Core_Matching"); + + b.HasIndex(new[] { "PartOfSpeech" }, "IX_OptionsVocabulary_PartOfSpeech"); + + b.HasIndex(new[] { "Word" }, "IX_OptionsVocabulary_Word") + .IsUnique(); + + b.HasIndex(new[] { "WordLength" }, "IX_OptionsVocabulary_WordLength"); + + b.ToTable("options_vocabularies", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccuracyScore") + .HasColumnType("TEXT") + .HasColumnName("accuracy_score"); + + b.Property("AudioUrl") + .HasColumnType("TEXT") + .HasColumnName("audio_url"); + + b.Property("CompletenessScore") + .HasColumnType("TEXT") + .HasColumnName("completeness_score"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("FluencyScore") + .HasColumnType("TEXT") + .HasColumnName("fluency_score"); + + b.Property("OverallScore") + .HasColumnType("INTEGER") + .HasColumnName("overall_score"); + + b.Property("PhonemeScores") + .HasColumnType("TEXT") + .HasColumnName("phoneme_scores"); + + b.Property("PracticeMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("practice_mode"); + + b.Property("ProsodyScore") + .HasColumnType("TEXT") + .HasColumnName("prosody_score"); + + b.Property("Suggestions") + .HasColumnType("TEXT") + .HasColumnName("suggestions"); + + b.Property("TargetText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("target_text"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("UserId", "FlashcardId") + .HasDatabaseName("IX_PronunciationAssessment_UserFlashcard"); + + b.ToTable("pronunciation_assessments", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.SentenceAnalysisCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AnalysisResult") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("analysis_result"); + + b.Property("CorrectedText") + .HasMaxLength(1000) + .HasColumnType("TEXT") + .HasColumnName("corrected_text"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires_at"); + + b.Property("GrammarCorrections") + .HasColumnType("TEXT") + .HasColumnName("grammar_corrections"); + + b.Property("HasGrammarErrors") + .HasColumnType("INTEGER") + .HasColumnName("has_grammar_errors"); + + b.Property("HighValueWords") + .HasColumnType("TEXT") + .HasColumnName("high_value_words"); + + b.Property("IdiomsDetected") + .HasColumnType("TEXT") + .HasColumnName("idioms_detected"); + + b.Property("InputText") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT") + .HasColumnName("input_text"); + + b.Property("InputTextHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("input_text_hash"); + + b.Property("LastAccessedAt") + .HasColumnType("TEXT") + .HasColumnName("last_accessed_at"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt") + .HasDatabaseName("IX_SentenceAnalysisCache_Expires"); + + b.HasIndex("InputTextHash") + .HasDatabaseName("IX_SentenceAnalysisCache_Hash"); + + b.HasIndex("InputTextHash", "ExpiresAt") + .HasDatabaseName("IX_SentenceAnalysisCache_Hash_Expires"); + + b.ToTable("SentenceAnalysisCache"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("UsageCount") + .HasColumnType("INTEGER") + .HasColumnName("usage_count"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("tags", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AvatarUrl") + .HasColumnType("TEXT") + .HasColumnName("avatar_url"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DisplayName") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("display_name"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("email"); + + b.Property("EnglishLevel") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("english_level"); + + b.Property("IsLevelVerified") + .HasColumnType("INTEGER") + .HasColumnName("is_level_verified"); + + b.Property("LevelNotes") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("level_notes"); + + b.Property("LevelUpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("level_updated_at"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("password_hash"); + + b.Property("Preferences") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("preferences"); + + b.Property("SubscriptionType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("subscription_type"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("user_profiles", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("AutoPlayEnabled") + .HasColumnType("INTEGER") + .HasColumnName("auto_play_enabled"); + + b.Property("DefaultSpeed") + .HasColumnType("TEXT") + .HasColumnName("default_speed"); + + b.Property("EnableDetailedFeedback") + .HasColumnType("INTEGER") + .HasColumnName("enable_detailed_feedback"); + + b.Property("PreferredAccent") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("preferred_accent"); + + b.Property("PreferredVoiceFemale") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("preferred_voice_female"); + + b.Property("PreferredVoiceMale") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("preferred_voice_male"); + + b.Property("PronunciationDifficulty") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("pronunciation_difficulty"); + + b.Property("TargetScoreThreshold") + .HasColumnType("INTEGER") + .HasColumnName("target_score_threshold"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("UserId"); + + b.ToTable("user_audio_preferences", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AutoPlayAudio") + .HasColumnType("INTEGER") + .HasColumnName("auto_play_audio"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DailyGoal") + .HasColumnType("INTEGER") + .HasColumnName("daily_goal"); + + b.Property("DifficultyPreference") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("difficulty_preference"); + + b.Property("ReminderEnabled") + .HasColumnType("INTEGER") + .HasColumnName("reminder_enabled"); + + b.Property("ReminderTime") + .HasColumnType("TEXT") + .HasColumnName("reminder_time"); + + b.Property("ShowPronunciation") + .HasColumnType("INTEGER") + .HasColumnName("show_pronunciation"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_settings", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("HighValueWordClicks") + .HasColumnType("INTEGER") + .HasColumnName("high_value_word_clicks"); + + b.Property("LowValueWordClicks") + .HasColumnType("INTEGER") + .HasColumnName("low_value_word_clicks"); + + b.Property("SentenceAnalysisCount") + .HasColumnType("INTEGER") + .HasColumnName("sentence_analysis_count"); + + b.Property("TotalApiCalls") + .HasColumnType("INTEGER") + .HasColumnName("total_api_calls"); + + b.Property("UniqueWordsQueried") + .HasColumnType("INTEGER") + .HasColumnName("unique_words_queried"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_WordQueryUsageStats_CreatedAt"); + + b.HasIndex("UserId", "Date") + .IsUnique() + .HasDatabaseName("IX_WordQueryUsageStats_UserDate"); + + b.ToTable("WordQueryUsageStats"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("DailyStats") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("ErrorReports") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.User", "ResolvedByUser") + .WithMany() + .HasForeignKey("ResolvedBy") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("ErrorReports") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("ResolvedByUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("Flashcards") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b => + { + b.HasOne("DramaLing.Api.Models.Entities.ExampleImage", "ExampleImage") + .WithMany("FlashcardExampleImages") + .HasForeignKey("ExampleImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("FlashcardExampleImages") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExampleImage"); + + b.Navigation("Flashcard"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("FlashcardTags") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.Tag", "Tag") + .WithMany("FlashcardTags") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.ExampleImage", "GeneratedImage") + .WithMany() + .HasForeignKey("GeneratedImageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("GeneratedImage"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithOne() + .HasForeignKey("DramaLing.Api.Models.Entities.UserAudioPreferences", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithOne("Settings") + .HasForeignKey("DramaLing.Api.Models.Entities.UserSettings", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => + { + b.Navigation("FlashcardExampleImages"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.Navigation("ErrorReports"); + + b.Navigation("FlashcardExampleImages"); + + b.Navigation("FlashcardTags"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.Navigation("FlashcardTags"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => + { + b.Navigation("DailyStats"); + + b.Navigation("ErrorReports"); + + b.Navigation("Flashcards"); + + b.Navigation("Settings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/DramaLing.Api/Migrations/20250930083856_CompleteSnakeCaseNaming.cs b/backend/DramaLing.Api/Migrations/20250930083856_CompleteSnakeCaseNaming.cs new file mode 100644 index 0000000..fbf5f19 --- /dev/null +++ b/backend/DramaLing.Api/Migrations/20250930083856_CompleteSnakeCaseNaming.cs @@ -0,0 +1,516 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DramaLing.Api.Migrations +{ + /// + public partial class CompleteSnakeCaseNaming : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_user_settings_user_profiles_UserId", + table: "user_settings"); + + migrationBuilder.DropForeignKey( + name: "FK_WordQueryUsageStats_user_profiles_UserId", + table: "WordQueryUsageStats"); + + migrationBuilder.RenameColumn( + name: "Date", + table: "WordQueryUsageStats", + newName: "date"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "WordQueryUsageStats", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "UserId", + table: "WordQueryUsageStats", + newName: "user_id"); + + migrationBuilder.RenameColumn( + name: "UpdatedAt", + table: "WordQueryUsageStats", + newName: "updated_at"); + + migrationBuilder.RenameColumn( + name: "UniqueWordsQueried", + table: "WordQueryUsageStats", + newName: "unique_words_queried"); + + migrationBuilder.RenameColumn( + name: "TotalApiCalls", + table: "WordQueryUsageStats", + newName: "total_api_calls"); + + migrationBuilder.RenameColumn( + name: "SentenceAnalysisCount", + table: "WordQueryUsageStats", + newName: "sentence_analysis_count"); + + migrationBuilder.RenameColumn( + name: "LowValueWordClicks", + table: "WordQueryUsageStats", + newName: "low_value_word_clicks"); + + migrationBuilder.RenameColumn( + name: "HighValueWordClicks", + table: "WordQueryUsageStats", + newName: "high_value_word_clicks"); + + migrationBuilder.RenameColumn( + name: "CreatedAt", + table: "WordQueryUsageStats", + newName: "created_at"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "user_settings", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "UserId", + table: "user_settings", + newName: "user_id"); + + migrationBuilder.RenameColumn( + name: "UpdatedAt", + table: "user_settings", + newName: "updated_at"); + + migrationBuilder.RenameColumn( + name: "ShowPronunciation", + table: "user_settings", + newName: "show_pronunciation"); + + migrationBuilder.RenameColumn( + name: "ReminderTime", + table: "user_settings", + newName: "reminder_time"); + + migrationBuilder.RenameColumn( + name: "ReminderEnabled", + table: "user_settings", + newName: "reminder_enabled"); + + migrationBuilder.RenameColumn( + name: "DifficultyPreference", + table: "user_settings", + newName: "difficulty_preference"); + + migrationBuilder.RenameColumn( + name: "DailyGoal", + table: "user_settings", + newName: "daily_goal"); + + migrationBuilder.RenameColumn( + name: "CreatedAt", + table: "user_settings", + newName: "created_at"); + + migrationBuilder.RenameColumn( + name: "AutoPlayAudio", + table: "user_settings", + newName: "auto_play_audio"); + + migrationBuilder.RenameIndex( + name: "IX_user_settings_UserId", + table: "user_settings", + newName: "IX_user_settings_user_id"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "user_profiles", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "Name", + table: "tags", + newName: "name"); + + migrationBuilder.RenameColumn( + name: "Color", + table: "tags", + newName: "color"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "tags", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "SentenceAnalysisCache", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "LastAccessedAt", + table: "SentenceAnalysisCache", + newName: "last_accessed_at"); + + migrationBuilder.RenameColumn( + name: "InputTextHash", + table: "SentenceAnalysisCache", + newName: "input_text_hash"); + + migrationBuilder.RenameColumn( + name: "InputText", + table: "SentenceAnalysisCache", + newName: "input_text"); + + migrationBuilder.RenameColumn( + name: "IdiomsDetected", + table: "SentenceAnalysisCache", + newName: "idioms_detected"); + + migrationBuilder.RenameColumn( + name: "HighValueWords", + table: "SentenceAnalysisCache", + newName: "high_value_words"); + + migrationBuilder.RenameColumn( + name: "HasGrammarErrors", + table: "SentenceAnalysisCache", + newName: "has_grammar_errors"); + + migrationBuilder.RenameColumn( + name: "GrammarCorrections", + table: "SentenceAnalysisCache", + newName: "grammar_corrections"); + + migrationBuilder.RenameColumn( + name: "ExpiresAt", + table: "SentenceAnalysisCache", + newName: "expires_at"); + + migrationBuilder.RenameColumn( + name: "CreatedAt", + table: "SentenceAnalysisCache", + newName: "created_at"); + + migrationBuilder.RenameColumn( + name: "CorrectedText", + table: "SentenceAnalysisCache", + newName: "corrected_text"); + + migrationBuilder.RenameColumn( + name: "AnalysisResult", + table: "SentenceAnalysisCache", + newName: "analysis_result"); + + migrationBuilder.RenameColumn( + name: "AccessCount", + table: "SentenceAnalysisCache", + newName: "access_count"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "flashcards", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "Status", + table: "error_reports", + newName: "status"); + + migrationBuilder.RenameColumn( + name: "Description", + table: "error_reports", + newName: "description"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "error_reports", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "Date", + table: "daily_stats", + newName: "date"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "daily_stats", + newName: "id"); + + migrationBuilder.RenameIndex( + name: "IX_daily_stats_user_id_Date", + table: "daily_stats", + newName: "IX_daily_stats_user_id_date"); + + migrationBuilder.AddForeignKey( + name: "FK_user_settings_user_profiles_user_id", + table: "user_settings", + column: "user_id", + principalTable: "user_profiles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_WordQueryUsageStats_user_profiles_user_id", + table: "WordQueryUsageStats", + column: "user_id", + principalTable: "user_profiles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_user_settings_user_profiles_user_id", + table: "user_settings"); + + migrationBuilder.DropForeignKey( + name: "FK_WordQueryUsageStats_user_profiles_user_id", + table: "WordQueryUsageStats"); + + migrationBuilder.RenameColumn( + name: "date", + table: "WordQueryUsageStats", + newName: "Date"); + + migrationBuilder.RenameColumn( + name: "id", + table: "WordQueryUsageStats", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "user_id", + table: "WordQueryUsageStats", + newName: "UserId"); + + migrationBuilder.RenameColumn( + name: "updated_at", + table: "WordQueryUsageStats", + newName: "UpdatedAt"); + + migrationBuilder.RenameColumn( + name: "unique_words_queried", + table: "WordQueryUsageStats", + newName: "UniqueWordsQueried"); + + migrationBuilder.RenameColumn( + name: "total_api_calls", + table: "WordQueryUsageStats", + newName: "TotalApiCalls"); + + migrationBuilder.RenameColumn( + name: "sentence_analysis_count", + table: "WordQueryUsageStats", + newName: "SentenceAnalysisCount"); + + migrationBuilder.RenameColumn( + name: "low_value_word_clicks", + table: "WordQueryUsageStats", + newName: "LowValueWordClicks"); + + migrationBuilder.RenameColumn( + name: "high_value_word_clicks", + table: "WordQueryUsageStats", + newName: "HighValueWordClicks"); + + migrationBuilder.RenameColumn( + name: "created_at", + table: "WordQueryUsageStats", + newName: "CreatedAt"); + + migrationBuilder.RenameColumn( + name: "id", + table: "user_settings", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "user_id", + table: "user_settings", + newName: "UserId"); + + migrationBuilder.RenameColumn( + name: "updated_at", + table: "user_settings", + newName: "UpdatedAt"); + + migrationBuilder.RenameColumn( + name: "show_pronunciation", + table: "user_settings", + newName: "ShowPronunciation"); + + migrationBuilder.RenameColumn( + name: "reminder_time", + table: "user_settings", + newName: "ReminderTime"); + + migrationBuilder.RenameColumn( + name: "reminder_enabled", + table: "user_settings", + newName: "ReminderEnabled"); + + migrationBuilder.RenameColumn( + name: "difficulty_preference", + table: "user_settings", + newName: "DifficultyPreference"); + + migrationBuilder.RenameColumn( + name: "daily_goal", + table: "user_settings", + newName: "DailyGoal"); + + migrationBuilder.RenameColumn( + name: "created_at", + table: "user_settings", + newName: "CreatedAt"); + + migrationBuilder.RenameColumn( + name: "auto_play_audio", + table: "user_settings", + newName: "AutoPlayAudio"); + + migrationBuilder.RenameIndex( + name: "IX_user_settings_user_id", + table: "user_settings", + newName: "IX_user_settings_UserId"); + + migrationBuilder.RenameColumn( + name: "id", + table: "user_profiles", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "name", + table: "tags", + newName: "Name"); + + migrationBuilder.RenameColumn( + name: "color", + table: "tags", + newName: "Color"); + + migrationBuilder.RenameColumn( + name: "id", + table: "tags", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "id", + table: "SentenceAnalysisCache", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "last_accessed_at", + table: "SentenceAnalysisCache", + newName: "LastAccessedAt"); + + migrationBuilder.RenameColumn( + name: "input_text_hash", + table: "SentenceAnalysisCache", + newName: "InputTextHash"); + + migrationBuilder.RenameColumn( + name: "input_text", + table: "SentenceAnalysisCache", + newName: "InputText"); + + migrationBuilder.RenameColumn( + name: "idioms_detected", + table: "SentenceAnalysisCache", + newName: "IdiomsDetected"); + + migrationBuilder.RenameColumn( + name: "high_value_words", + table: "SentenceAnalysisCache", + newName: "HighValueWords"); + + migrationBuilder.RenameColumn( + name: "has_grammar_errors", + table: "SentenceAnalysisCache", + newName: "HasGrammarErrors"); + + migrationBuilder.RenameColumn( + name: "grammar_corrections", + table: "SentenceAnalysisCache", + newName: "GrammarCorrections"); + + migrationBuilder.RenameColumn( + name: "expires_at", + table: "SentenceAnalysisCache", + newName: "ExpiresAt"); + + migrationBuilder.RenameColumn( + name: "created_at", + table: "SentenceAnalysisCache", + newName: "CreatedAt"); + + migrationBuilder.RenameColumn( + name: "corrected_text", + table: "SentenceAnalysisCache", + newName: "CorrectedText"); + + migrationBuilder.RenameColumn( + name: "analysis_result", + table: "SentenceAnalysisCache", + newName: "AnalysisResult"); + + migrationBuilder.RenameColumn( + name: "access_count", + table: "SentenceAnalysisCache", + newName: "AccessCount"); + + migrationBuilder.RenameColumn( + name: "id", + table: "flashcards", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "status", + table: "error_reports", + newName: "Status"); + + migrationBuilder.RenameColumn( + name: "description", + table: "error_reports", + newName: "Description"); + + migrationBuilder.RenameColumn( + name: "id", + table: "error_reports", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "date", + table: "daily_stats", + newName: "Date"); + + migrationBuilder.RenameColumn( + name: "id", + table: "daily_stats", + newName: "Id"); + + migrationBuilder.RenameIndex( + name: "IX_daily_stats_user_id_date", + table: "daily_stats", + newName: "IX_daily_stats_user_id_Date"); + + migrationBuilder.AddForeignKey( + name: "FK_user_settings_user_profiles_UserId", + table: "user_settings", + column: "UserId", + principalTable: "user_profiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_WordQueryUsageStats_user_profiles_UserId", + table: "WordQueryUsageStats", + column: "UserId", + principalTable: "user_profiles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/backend/DramaLing.Api/Migrations/20250930084945_FinalPascalCaseFieldsFix.Designer.cs b/backend/DramaLing.Api/Migrations/20250930084945_FinalPascalCaseFieldsFix.Designer.cs new file mode 100644 index 0000000..9324ad6 --- /dev/null +++ b/backend/DramaLing.Api/Migrations/20250930084945_FinalPascalCaseFieldsFix.Designer.cs @@ -0,0 +1,1252 @@ +// +using System; +using DramaLing.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DramaLing.Api.Migrations +{ + [DbContext(typeof(DramaLingDbContext))] + [Migration("20250930084945_FinalPascalCaseFieldsFix")] + partial class FinalPascalCaseFieldsFix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.AudioCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Accent") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("accent"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AudioUrl") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("audio_url"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DurationMs") + .HasColumnType("INTEGER") + .HasColumnName("duration_ms"); + + b.Property("FileSize") + .HasColumnType("INTEGER") + .HasColumnName("file_size"); + + b.Property("LastAccessed") + .HasColumnType("TEXT") + .HasColumnName("last_accessed"); + + b.Property("TextContent") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("text_content"); + + b.Property("TextHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("text_hash"); + + b.Property("VoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("voice_id"); + + b.HasKey("Id"); + + b.HasIndex("LastAccessed") + .HasDatabaseName("IX_AudioCache_LastAccessed"); + + b.HasIndex("TextHash") + .IsUnique() + .HasDatabaseName("IX_AudioCache_TextHash"); + + b.ToTable("audio_cache", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AiApiCalls") + .HasColumnType("INTEGER") + .HasColumnName("ai_api_calls"); + + b.Property("CardsGenerated") + .HasColumnType("INTEGER") + .HasColumnName("cards_generated"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("SessionCount") + .HasColumnType("INTEGER") + .HasColumnName("session_count"); + + b.Property("StudyTimeSeconds") + .HasColumnType("INTEGER") + .HasColumnName("study_time_seconds"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("WordsCorrect") + .HasColumnType("INTEGER") + .HasColumnName("words_correct"); + + b.Property("WordsStudied") + .HasColumnType("INTEGER") + .HasColumnName("words_studied"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Date") + .IsUnique(); + + b.ToTable("daily_stats", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AdminNotes") + .HasColumnType("TEXT") + .HasColumnName("admin_notes"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("ReportType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("report_type"); + + b.Property("ResolvedAt") + .HasColumnType("TEXT") + .HasColumnName("resolved_at"); + + b.Property("ResolvedBy") + .HasColumnType("TEXT") + .HasColumnName("resolved_by"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("StudyMode") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("study_mode"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("ResolvedBy"); + + b.HasIndex("UserId"); + + b.ToTable("error_reports", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AltText") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("alt_text"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("content_hash"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FileSize") + .HasColumnType("INTEGER") + .HasColumnName("file_size"); + + b.Property("GeminiCost") + .HasColumnType("TEXT") + .HasColumnName("gemini_cost"); + + b.Property("GeminiDescription") + .HasColumnType("TEXT") + .HasColumnName("gemini_description"); + + b.Property("GeminiPrompt") + .HasColumnType("TEXT") + .HasColumnName("gemini_prompt"); + + b.Property("ImageHeight") + .HasColumnType("INTEGER") + .HasColumnName("image_height"); + + b.Property("ImageWidth") + .HasColumnType("INTEGER") + .HasColumnName("image_width"); + + b.Property("ModerationNotes") + .HasColumnType("TEXT") + .HasColumnName("moderation_notes"); + + b.Property("ModerationStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("moderation_status"); + + b.Property("QualityScore") + .HasColumnType("TEXT") + .HasColumnName("quality_score"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("relative_path"); + + b.Property("ReplicateCost") + .HasColumnType("TEXT") + .HasColumnName("replicate_cost"); + + b.Property("ReplicateModel") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("replicate_model"); + + b.Property("ReplicatePrompt") + .HasColumnType("TEXT") + .HasColumnName("replicate_prompt"); + + b.Property("ReplicateVersion") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("replicate_version"); + + b.Property("TotalGenerationCost") + .HasColumnType("TEXT") + .HasColumnName("total_generation_cost"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("AccessCount"); + + b.HasIndex("ContentHash") + .IsUnique(); + + b.ToTable("example_images", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Definition") + .HasColumnType("TEXT") + .HasColumnName("definition"); + + b.Property("DifficultyLevel") + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("difficulty_level"); + + b.Property("Example") + .HasColumnType("TEXT") + .HasColumnName("example"); + + b.Property("ExampleTranslation") + .HasColumnType("TEXT") + .HasColumnName("example_translation"); + + b.Property("IsArchived") + .HasColumnType("INTEGER") + .HasColumnName("is_archived"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER") + .HasColumnName("is_favorite"); + + b.Property("PartOfSpeech") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("part_of_speech"); + + b.Property("Pronunciation") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("pronunciation"); + + b.Property("Translation") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("translation"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("word"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("flashcards", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b => + { + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("ExampleImageId") + .HasColumnType("TEXT") + .HasColumnName("example_image_id"); + + b.Property("ContextRelevance") + .HasColumnType("TEXT") + .HasColumnName("context_relevance"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER") + .HasColumnName("display_order"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER") + .HasColumnName("is_primary"); + + b.HasKey("FlashcardId", "ExampleImageId"); + + b.HasIndex("ExampleImageId"); + + b.ToTable("flashcard_example_images", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b => + { + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("TagId") + .HasColumnType("TEXT") + .HasColumnName("tag_id"); + + b.HasKey("FlashcardId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("flashcard_tags", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CompletedAt") + .HasColumnType("TEXT") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FinalReplicatePrompt") + .HasColumnType("TEXT") + .HasColumnName("final_replicate_prompt"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("GeminiCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("gemini_completed_at"); + + b.Property("GeminiCost") + .HasColumnType("TEXT") + .HasColumnName("gemini_cost"); + + b.Property("GeminiErrorMessage") + .HasColumnType("TEXT") + .HasColumnName("gemini_error_message"); + + b.Property("GeminiProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("gemini_processing_time_ms"); + + b.Property("GeminiPrompt") + .HasColumnType("TEXT") + .HasColumnName("gemini_prompt"); + + b.Property("GeminiStartedAt") + .HasColumnType("TEXT") + .HasColumnName("gemini_started_at"); + + b.Property("GeminiStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("gemini_status"); + + b.Property("GeneratedDescription") + .HasColumnType("TEXT") + .HasColumnName("generated_description"); + + b.Property("GeneratedImageId") + .HasColumnType("TEXT") + .HasColumnName("generated_image_id"); + + b.Property("OriginalRequest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("original_request"); + + b.Property("OverallStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("overall_status"); + + b.Property("ReplicateCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("replicate_completed_at"); + + b.Property("ReplicateCost") + .HasColumnType("TEXT") + .HasColumnName("replicate_cost"); + + b.Property("ReplicateErrorMessage") + .HasColumnType("TEXT") + .HasColumnName("replicate_error_message"); + + b.Property("ReplicateProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("replicate_processing_time_ms"); + + b.Property("ReplicateStartedAt") + .HasColumnType("TEXT") + .HasColumnName("replicate_started_at"); + + b.Property("ReplicateStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("replicate_status"); + + b.Property("TotalCost") + .HasColumnType("TEXT") + .HasColumnName("total_cost"); + + b.Property("TotalProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("total_processing_time_ms"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("GeneratedImageId"); + + b.HasIndex("UserId"); + + b.ToTable("image_generation_requests", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.OptionsVocabulary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CEFRLevel") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("cefr_level"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("PartOfSpeech") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("part_of_speech"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("updated_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("word"); + + b.Property("WordLength") + .HasColumnType("INTEGER") + .HasColumnName("word_length"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsActive" }, "IX_OptionsVocabulary_Active"); + + b.HasIndex(new[] { "CEFRLevel" }, "IX_OptionsVocabulary_CEFR"); + + b.HasIndex(new[] { "CEFRLevel", "PartOfSpeech", "WordLength" }, "IX_OptionsVocabulary_Core_Matching"); + + b.HasIndex(new[] { "PartOfSpeech" }, "IX_OptionsVocabulary_PartOfSpeech"); + + b.HasIndex(new[] { "Word" }, "IX_OptionsVocabulary_Word") + .IsUnique(); + + b.HasIndex(new[] { "WordLength" }, "IX_OptionsVocabulary_WordLength"); + + b.ToTable("options_vocabularies", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AccuracyScore") + .HasColumnType("TEXT") + .HasColumnName("accuracy_score"); + + b.Property("AudioUrl") + .HasColumnType("TEXT") + .HasColumnName("audio_url"); + + b.Property("CompletenessScore") + .HasColumnType("TEXT") + .HasColumnName("completeness_score"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("FluencyScore") + .HasColumnType("TEXT") + .HasColumnName("fluency_score"); + + b.Property("OverallScore") + .HasColumnType("INTEGER") + .HasColumnName("overall_score"); + + b.Property("PhonemeScores") + .HasColumnType("TEXT") + .HasColumnName("phoneme_scores"); + + b.Property("PracticeMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("practice_mode"); + + b.Property("ProsodyScore") + .HasColumnType("TEXT") + .HasColumnName("prosody_score"); + + b.Property("Suggestions") + .HasColumnType("TEXT") + .HasColumnName("suggestions"); + + b.Property("TargetText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("target_text"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("UserId", "FlashcardId") + .HasDatabaseName("IX_PronunciationAssessment_UserFlashcard"); + + b.ToTable("pronunciation_assessments", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.SentenceAnalysisCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AnalysisResult") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("analysis_result"); + + b.Property("CorrectedText") + .HasMaxLength(1000) + .HasColumnType("TEXT") + .HasColumnName("corrected_text"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires_at"); + + b.Property("GrammarCorrections") + .HasColumnType("TEXT") + .HasColumnName("grammar_corrections"); + + b.Property("HasGrammarErrors") + .HasColumnType("INTEGER") + .HasColumnName("has_grammar_errors"); + + b.Property("HighValueWords") + .HasColumnType("TEXT") + .HasColumnName("high_value_words"); + + b.Property("IdiomsDetected") + .HasColumnType("TEXT") + .HasColumnName("idioms_detected"); + + b.Property("InputText") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT") + .HasColumnName("input_text"); + + b.Property("InputTextHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("input_text_hash"); + + b.Property("LastAccessedAt") + .HasColumnType("TEXT") + .HasColumnName("last_accessed_at"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt") + .HasDatabaseName("IX_SentenceAnalysisCache_Expires"); + + b.HasIndex("InputTextHash") + .HasDatabaseName("IX_SentenceAnalysisCache_Hash"); + + b.HasIndex("InputTextHash", "ExpiresAt") + .HasDatabaseName("IX_SentenceAnalysisCache_Hash_Expires"); + + b.ToTable("SentenceAnalysisCache"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("UsageCount") + .HasColumnType("INTEGER") + .HasColumnName("usage_count"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("tags", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AvatarUrl") + .HasColumnType("TEXT") + .HasColumnName("avatar_url"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DisplayName") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("display_name"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("email"); + + b.Property("EnglishLevel") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("english_level"); + + b.Property("IsLevelVerified") + .HasColumnType("INTEGER") + .HasColumnName("is_level_verified"); + + b.Property("LevelNotes") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("level_notes"); + + b.Property("LevelUpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("level_updated_at"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("password_hash"); + + b.Property("Preferences") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("preferences"); + + b.Property("SubscriptionType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("subscription_type"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("user_profiles", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => + { + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("AutoPlayEnabled") + .HasColumnType("INTEGER") + .HasColumnName("auto_play_enabled"); + + b.Property("DefaultSpeed") + .HasColumnType("TEXT") + .HasColumnName("default_speed"); + + b.Property("EnableDetailedFeedback") + .HasColumnType("INTEGER") + .HasColumnName("enable_detailed_feedback"); + + b.Property("PreferredAccent") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("preferred_accent"); + + b.Property("PreferredVoiceFemale") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("preferred_voice_female"); + + b.Property("PreferredVoiceMale") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("preferred_voice_male"); + + b.Property("PronunciationDifficulty") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("pronunciation_difficulty"); + + b.Property("TargetScoreThreshold") + .HasColumnType("INTEGER") + .HasColumnName("target_score_threshold"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("UserId"); + + b.ToTable("user_audio_preferences", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AutoPlayAudio") + .HasColumnType("INTEGER") + .HasColumnName("auto_play_audio"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DailyGoal") + .HasColumnType("INTEGER") + .HasColumnName("daily_goal"); + + b.Property("DifficultyPreference") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("difficulty_preference"); + + b.Property("ReminderEnabled") + .HasColumnType("INTEGER") + .HasColumnName("reminder_enabled"); + + b.Property("ReminderTime") + .HasColumnType("TEXT") + .HasColumnName("reminder_time"); + + b.Property("ShowPronunciation") + .HasColumnType("INTEGER") + .HasColumnName("show_pronunciation"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_settings", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("HighValueWordClicks") + .HasColumnType("INTEGER") + .HasColumnName("high_value_word_clicks"); + + b.Property("LowValueWordClicks") + .HasColumnType("INTEGER") + .HasColumnName("low_value_word_clicks"); + + b.Property("SentenceAnalysisCount") + .HasColumnType("INTEGER") + .HasColumnName("sentence_analysis_count"); + + b.Property("TotalApiCalls") + .HasColumnType("INTEGER") + .HasColumnName("total_api_calls"); + + b.Property("UniqueWordsQueried") + .HasColumnType("INTEGER") + .HasColumnName("unique_words_queried"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_WordQueryUsageStats_CreatedAt"); + + b.HasIndex("UserId", "Date") + .IsUnique() + .HasDatabaseName("IX_WordQueryUsageStats_UserDate"); + + b.ToTable("WordQueryUsageStats"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("DailyStats") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("ErrorReports") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.User", "ResolvedByUser") + .WithMany() + .HasForeignKey("ResolvedBy") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("ErrorReports") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("ResolvedByUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("Flashcards") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b => + { + b.HasOne("DramaLing.Api.Models.Entities.ExampleImage", "ExampleImage") + .WithMany("FlashcardExampleImages") + .HasForeignKey("ExampleImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("FlashcardExampleImages") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExampleImage"); + + b.Navigation("Flashcard"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("FlashcardTags") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.Tag", "Tag") + .WithMany("FlashcardTags") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.ExampleImage", "GeneratedImage") + .WithMany() + .HasForeignKey("GeneratedImageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("GeneratedImage"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithOne() + .HasForeignKey("DramaLing.Api.Models.Entities.UserAudioPreferences", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithOne("Settings") + .HasForeignKey("DramaLing.Api.Models.Entities.UserSettings", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => + { + b.Navigation("FlashcardExampleImages"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.Navigation("ErrorReports"); + + b.Navigation("FlashcardExampleImages"); + + b.Navigation("FlashcardTags"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.Navigation("FlashcardTags"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => + { + b.Navigation("DailyStats"); + + b.Navigation("ErrorReports"); + + b.Navigation("Flashcards"); + + b.Navigation("Settings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/DramaLing.Api/Migrations/20250930084945_FinalPascalCaseFieldsFix.cs b/backend/DramaLing.Api/Migrations/20250930084945_FinalPascalCaseFieldsFix.cs new file mode 100644 index 0000000..aa05ccd --- /dev/null +++ b/backend/DramaLing.Api/Migrations/20250930084945_FinalPascalCaseFieldsFix.cs @@ -0,0 +1,122 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DramaLing.Api.Migrations +{ + /// + public partial class FinalPascalCaseFieldsFix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_user_audio_preferences_user_profiles_UserId", + table: "user_audio_preferences"); + + migrationBuilder.RenameColumn( + name: "UserId", + table: "user_audio_preferences", + newName: "user_id"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "pronunciation_assessments", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "Word", + table: "options_vocabularies", + newName: "word"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "options_vocabularies", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "image_generation_requests", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "example_images", + newName: "id"); + + migrationBuilder.RenameColumn( + name: "Accent", + table: "audio_cache", + newName: "accent"); + + migrationBuilder.RenameColumn( + name: "Id", + table: "audio_cache", + newName: "id"); + + migrationBuilder.AddForeignKey( + name: "FK_user_audio_preferences_user_profiles_user_id", + table: "user_audio_preferences", + column: "user_id", + principalTable: "user_profiles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_user_audio_preferences_user_profiles_user_id", + table: "user_audio_preferences"); + + migrationBuilder.RenameColumn( + name: "user_id", + table: "user_audio_preferences", + newName: "UserId"); + + migrationBuilder.RenameColumn( + name: "id", + table: "pronunciation_assessments", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "word", + table: "options_vocabularies", + newName: "Word"); + + migrationBuilder.RenameColumn( + name: "id", + table: "options_vocabularies", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "id", + table: "image_generation_requests", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "id", + table: "example_images", + newName: "Id"); + + migrationBuilder.RenameColumn( + name: "accent", + table: "audio_cache", + newName: "Accent"); + + migrationBuilder.RenameColumn( + name: "id", + table: "audio_cache", + newName: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_user_audio_preferences_user_profiles_UserId", + table: "user_audio_preferences", + column: "UserId", + principalTable: "user_profiles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/backend/DramaLing.Api/Migrations/20250930085131_FixTableNamesToSnakeCase.Designer.cs b/backend/DramaLing.Api/Migrations/20250930085131_FixTableNamesToSnakeCase.Designer.cs new file mode 100644 index 0000000..3c4e84f --- /dev/null +++ b/backend/DramaLing.Api/Migrations/20250930085131_FixTableNamesToSnakeCase.Designer.cs @@ -0,0 +1,1252 @@ +// +using System; +using DramaLing.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DramaLing.Api.Migrations +{ + [DbContext(typeof(DramaLingDbContext))] + [Migration("20250930085131_FixTableNamesToSnakeCase")] + partial class FixTableNamesToSnakeCase + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.AudioCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Accent") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("accent"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AudioUrl") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("audio_url"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DurationMs") + .HasColumnType("INTEGER") + .HasColumnName("duration_ms"); + + b.Property("FileSize") + .HasColumnType("INTEGER") + .HasColumnName("file_size"); + + b.Property("LastAccessed") + .HasColumnType("TEXT") + .HasColumnName("last_accessed"); + + b.Property("TextContent") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("text_content"); + + b.Property("TextHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("text_hash"); + + b.Property("VoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("voice_id"); + + b.HasKey("Id"); + + b.HasIndex("LastAccessed") + .HasDatabaseName("IX_AudioCache_LastAccessed"); + + b.HasIndex("TextHash") + .IsUnique() + .HasDatabaseName("IX_AudioCache_TextHash"); + + b.ToTable("audio_cache", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AiApiCalls") + .HasColumnType("INTEGER") + .HasColumnName("ai_api_calls"); + + b.Property("CardsGenerated") + .HasColumnType("INTEGER") + .HasColumnName("cards_generated"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("SessionCount") + .HasColumnType("INTEGER") + .HasColumnName("session_count"); + + b.Property("StudyTimeSeconds") + .HasColumnType("INTEGER") + .HasColumnName("study_time_seconds"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("WordsCorrect") + .HasColumnType("INTEGER") + .HasColumnName("words_correct"); + + b.Property("WordsStudied") + .HasColumnType("INTEGER") + .HasColumnName("words_studied"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Date") + .IsUnique(); + + b.ToTable("daily_stats", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AdminNotes") + .HasColumnType("TEXT") + .HasColumnName("admin_notes"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("ReportType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("report_type"); + + b.Property("ResolvedAt") + .HasColumnType("TEXT") + .HasColumnName("resolved_at"); + + b.Property("ResolvedBy") + .HasColumnType("TEXT") + .HasColumnName("resolved_by"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("StudyMode") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("study_mode"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("ResolvedBy"); + + b.HasIndex("UserId"); + + b.ToTable("error_reports", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AltText") + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("alt_text"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("content_hash"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FileSize") + .HasColumnType("INTEGER") + .HasColumnName("file_size"); + + b.Property("GeminiCost") + .HasColumnType("TEXT") + .HasColumnName("gemini_cost"); + + b.Property("GeminiDescription") + .HasColumnType("TEXT") + .HasColumnName("gemini_description"); + + b.Property("GeminiPrompt") + .HasColumnType("TEXT") + .HasColumnName("gemini_prompt"); + + b.Property("ImageHeight") + .HasColumnType("INTEGER") + .HasColumnName("image_height"); + + b.Property("ImageWidth") + .HasColumnType("INTEGER") + .HasColumnName("image_width"); + + b.Property("ModerationNotes") + .HasColumnType("TEXT") + .HasColumnName("moderation_notes"); + + b.Property("ModerationStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("moderation_status"); + + b.Property("QualityScore") + .HasColumnType("TEXT") + .HasColumnName("quality_score"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("relative_path"); + + b.Property("ReplicateCost") + .HasColumnType("TEXT") + .HasColumnName("replicate_cost"); + + b.Property("ReplicateModel") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("replicate_model"); + + b.Property("ReplicatePrompt") + .HasColumnType("TEXT") + .HasColumnName("replicate_prompt"); + + b.Property("ReplicateVersion") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("replicate_version"); + + b.Property("TotalGenerationCost") + .HasColumnType("TEXT") + .HasColumnName("total_generation_cost"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("AccessCount"); + + b.HasIndex("ContentHash") + .IsUnique(); + + b.ToTable("example_images", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Definition") + .HasColumnType("TEXT") + .HasColumnName("definition"); + + b.Property("DifficultyLevel") + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("difficulty_level"); + + b.Property("Example") + .HasColumnType("TEXT") + .HasColumnName("example"); + + b.Property("ExampleTranslation") + .HasColumnType("TEXT") + .HasColumnName("example_translation"); + + b.Property("IsArchived") + .HasColumnType("INTEGER") + .HasColumnName("is_archived"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER") + .HasColumnName("is_favorite"); + + b.Property("PartOfSpeech") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("part_of_speech"); + + b.Property("Pronunciation") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("pronunciation"); + + b.Property("Translation") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("translation"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("word"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("flashcards", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b => + { + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("ExampleImageId") + .HasColumnType("TEXT") + .HasColumnName("example_image_id"); + + b.Property("ContextRelevance") + .HasColumnType("TEXT") + .HasColumnName("context_relevance"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER") + .HasColumnName("display_order"); + + b.Property("IsPrimary") + .HasColumnType("INTEGER") + .HasColumnName("is_primary"); + + b.HasKey("FlashcardId", "ExampleImageId"); + + b.HasIndex("ExampleImageId"); + + b.ToTable("flashcard_example_images", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b => + { + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("TagId") + .HasColumnType("TEXT") + .HasColumnName("tag_id"); + + b.HasKey("FlashcardId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("flashcard_tags", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CompletedAt") + .HasColumnType("TEXT") + .HasColumnName("completed_at"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FinalReplicatePrompt") + .HasColumnType("TEXT") + .HasColumnName("final_replicate_prompt"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("GeminiCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("gemini_completed_at"); + + b.Property("GeminiCost") + .HasColumnType("TEXT") + .HasColumnName("gemini_cost"); + + b.Property("GeminiErrorMessage") + .HasColumnType("TEXT") + .HasColumnName("gemini_error_message"); + + b.Property("GeminiProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("gemini_processing_time_ms"); + + b.Property("GeminiPrompt") + .HasColumnType("TEXT") + .HasColumnName("gemini_prompt"); + + b.Property("GeminiStartedAt") + .HasColumnType("TEXT") + .HasColumnName("gemini_started_at"); + + b.Property("GeminiStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("gemini_status"); + + b.Property("GeneratedDescription") + .HasColumnType("TEXT") + .HasColumnName("generated_description"); + + b.Property("GeneratedImageId") + .HasColumnType("TEXT") + .HasColumnName("generated_image_id"); + + b.Property("OriginalRequest") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("original_request"); + + b.Property("OverallStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("overall_status"); + + b.Property("ReplicateCompletedAt") + .HasColumnType("TEXT") + .HasColumnName("replicate_completed_at"); + + b.Property("ReplicateCost") + .HasColumnType("TEXT") + .HasColumnName("replicate_cost"); + + b.Property("ReplicateErrorMessage") + .HasColumnType("TEXT") + .HasColumnName("replicate_error_message"); + + b.Property("ReplicateProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("replicate_processing_time_ms"); + + b.Property("ReplicateStartedAt") + .HasColumnType("TEXT") + .HasColumnName("replicate_started_at"); + + b.Property("ReplicateStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("replicate_status"); + + b.Property("TotalCost") + .HasColumnType("TEXT") + .HasColumnName("total_cost"); + + b.Property("TotalProcessingTimeMs") + .HasColumnType("INTEGER") + .HasColumnName("total_processing_time_ms"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("GeneratedImageId"); + + b.HasIndex("UserId"); + + b.ToTable("image_generation_requests", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.OptionsVocabulary", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CEFRLevel") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("cefr_level"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("created_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true) + .HasColumnName("is_active"); + + b.Property("PartOfSpeech") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("part_of_speech"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("updated_at") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Word") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("word"); + + b.Property("WordLength") + .HasColumnType("INTEGER") + .HasColumnName("word_length"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "IsActive" }, "IX_OptionsVocabulary_Active"); + + b.HasIndex(new[] { "CEFRLevel" }, "IX_OptionsVocabulary_CEFR"); + + b.HasIndex(new[] { "CEFRLevel", "PartOfSpeech", "WordLength" }, "IX_OptionsVocabulary_Core_Matching"); + + b.HasIndex(new[] { "PartOfSpeech" }, "IX_OptionsVocabulary_PartOfSpeech"); + + b.HasIndex(new[] { "Word" }, "IX_OptionsVocabulary_Word") + .IsUnique(); + + b.HasIndex(new[] { "WordLength" }, "IX_OptionsVocabulary_WordLength"); + + b.ToTable("options_vocabularies", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AccuracyScore") + .HasColumnType("TEXT") + .HasColumnName("accuracy_score"); + + b.Property("AudioUrl") + .HasColumnType("TEXT") + .HasColumnName("audio_url"); + + b.Property("CompletenessScore") + .HasColumnType("TEXT") + .HasColumnName("completeness_score"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("FlashcardId") + .HasColumnType("TEXT") + .HasColumnName("flashcard_id"); + + b.Property("FluencyScore") + .HasColumnType("TEXT") + .HasColumnName("fluency_score"); + + b.Property("OverallScore") + .HasColumnType("INTEGER") + .HasColumnName("overall_score"); + + b.Property("PhonemeScores") + .HasColumnType("TEXT") + .HasColumnName("phoneme_scores"); + + b.Property("PracticeMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("practice_mode"); + + b.Property("ProsodyScore") + .HasColumnType("TEXT") + .HasColumnName("prosody_score"); + + b.Property("Suggestions") + .HasColumnType("TEXT") + .HasColumnName("suggestions"); + + b.Property("TargetText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("target_text"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("FlashcardId"); + + b.HasIndex("UserId", "FlashcardId") + .HasDatabaseName("IX_PronunciationAssessment_UserFlashcard"); + + b.ToTable("pronunciation_assessments", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.SentenceAnalysisCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AccessCount") + .HasColumnType("INTEGER") + .HasColumnName("access_count"); + + b.Property("AnalysisResult") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("analysis_result"); + + b.Property("CorrectedText") + .HasMaxLength(1000) + .HasColumnType("TEXT") + .HasColumnName("corrected_text"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT") + .HasColumnName("expires_at"); + + b.Property("GrammarCorrections") + .HasColumnType("TEXT") + .HasColumnName("grammar_corrections"); + + b.Property("HasGrammarErrors") + .HasColumnType("INTEGER") + .HasColumnName("has_grammar_errors"); + + b.Property("HighValueWords") + .HasColumnType("TEXT") + .HasColumnName("high_value_words"); + + b.Property("IdiomsDetected") + .HasColumnType("TEXT") + .HasColumnName("idioms_detected"); + + b.Property("InputText") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("TEXT") + .HasColumnName("input_text"); + + b.Property("InputTextHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT") + .HasColumnName("input_text_hash"); + + b.Property("LastAccessedAt") + .HasColumnType("TEXT") + .HasColumnName("last_accessed_at"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt") + .HasDatabaseName("IX_SentenceAnalysisCache_Expires"); + + b.HasIndex("InputTextHash") + .HasDatabaseName("IX_SentenceAnalysisCache_Hash"); + + b.HasIndex("InputTextHash", "ExpiresAt") + .HasDatabaseName("IX_SentenceAnalysisCache_Hash_Expires"); + + b.ToTable("sentence_analysis_cache", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("UsageCount") + .HasColumnType("INTEGER") + .HasColumnName("usage_count"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("tags", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AvatarUrl") + .HasColumnType("TEXT") + .HasColumnName("avatar_url"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DisplayName") + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("display_name"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("email"); + + b.Property("EnglishLevel") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT") + .HasColumnName("english_level"); + + b.Property("IsLevelVerified") + .HasColumnType("INTEGER") + .HasColumnName("is_level_verified"); + + b.Property("LevelNotes") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("level_notes"); + + b.Property("LevelUpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("level_updated_at"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("password_hash"); + + b.Property("Preferences") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("preferences"); + + b.Property("SubscriptionType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("subscription_type"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("user_profiles", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => + { + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("AutoPlayEnabled") + .HasColumnType("INTEGER") + .HasColumnName("auto_play_enabled"); + + b.Property("DefaultSpeed") + .HasColumnType("TEXT") + .HasColumnName("default_speed"); + + b.Property("EnableDetailedFeedback") + .HasColumnType("INTEGER") + .HasColumnName("enable_detailed_feedback"); + + b.Property("PreferredAccent") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("TEXT") + .HasColumnName("preferred_accent"); + + b.Property("PreferredVoiceFemale") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("preferred_voice_female"); + + b.Property("PreferredVoiceMale") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("preferred_voice_male"); + + b.Property("PronunciationDifficulty") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("pronunciation_difficulty"); + + b.Property("TargetScoreThreshold") + .HasColumnType("INTEGER") + .HasColumnName("target_score_threshold"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("UserId"); + + b.ToTable("user_audio_preferences", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AutoPlayAudio") + .HasColumnType("INTEGER") + .HasColumnName("auto_play_audio"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("DailyGoal") + .HasColumnType("INTEGER") + .HasColumnName("daily_goal"); + + b.Property("DifficultyPreference") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT") + .HasColumnName("difficulty_preference"); + + b.Property("ReminderEnabled") + .HasColumnType("INTEGER") + .HasColumnName("reminder_enabled"); + + b.Property("ReminderTime") + .HasColumnType("TEXT") + .HasColumnName("reminder_time"); + + b.Property("ShowPronunciation") + .HasColumnType("INTEGER") + .HasColumnName("show_pronunciation"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_settings", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("HighValueWordClicks") + .HasColumnType("INTEGER") + .HasColumnName("high_value_word_clicks"); + + b.Property("LowValueWordClicks") + .HasColumnType("INTEGER") + .HasColumnName("low_value_word_clicks"); + + b.Property("SentenceAnalysisCount") + .HasColumnType("INTEGER") + .HasColumnName("sentence_analysis_count"); + + b.Property("TotalApiCalls") + .HasColumnType("INTEGER") + .HasColumnName("total_api_calls"); + + b.Property("UniqueWordsQueried") + .HasColumnType("INTEGER") + .HasColumnName("unique_words_queried"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_WordQueryUsageStats_CreatedAt"); + + b.HasIndex("UserId", "Date") + .IsUnique() + .HasDatabaseName("IX_WordQueryUsageStats_UserDate"); + + b.ToTable("word_query_usage_stats", (string)null); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("DailyStats") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ErrorReport", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("ErrorReports") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.User", "ResolvedByUser") + .WithMany() + .HasForeignKey("ResolvedBy") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("ErrorReports") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("ResolvedByUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany("Flashcards") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardExampleImage", b => + { + b.HasOne("DramaLing.Api.Models.Entities.ExampleImage", "ExampleImage") + .WithMany("FlashcardExampleImages") + .HasForeignKey("ExampleImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("FlashcardExampleImages") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ExampleImage"); + + b.Navigation("Flashcard"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.FlashcardTag", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany("FlashcardTags") + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.Tag", "Tag") + .WithMany("FlashcardTags") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ImageGenerationRequest", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DramaLing.Api.Models.Entities.ExampleImage", "GeneratedImage") + .WithMany() + .HasForeignKey("GeneratedImageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("GeneratedImage"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.PronunciationAssessment", b => + { + b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") + .WithMany() + .HasForeignKey("FlashcardId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Flashcard"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithOne() + .HasForeignKey("DramaLing.Api.Models.Entities.UserAudioPreferences", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.UserSettings", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithOne("Settings") + .HasForeignKey("DramaLing.Api.Models.Entities.UserSettings", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.WordQueryUsageStats", b => + { + b.HasOne("DramaLing.Api.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b => + { + b.Navigation("FlashcardExampleImages"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b => + { + b.Navigation("ErrorReports"); + + b.Navigation("FlashcardExampleImages"); + + b.Navigation("FlashcardTags"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => + { + b.Navigation("FlashcardTags"); + }); + + modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => + { + b.Navigation("DailyStats"); + + b.Navigation("ErrorReports"); + + b.Navigation("Flashcards"); + + b.Navigation("Settings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/DramaLing.Api/Migrations/20250930085131_FixTableNamesToSnakeCase.cs b/backend/DramaLing.Api/Migrations/20250930085131_FixTableNamesToSnakeCase.cs new file mode 100644 index 0000000..d4f7cf4 --- /dev/null +++ b/backend/DramaLing.Api/Migrations/20250930085131_FixTableNamesToSnakeCase.cs @@ -0,0 +1,94 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DramaLing.Api.Migrations +{ + /// + public partial class FixTableNamesToSnakeCase : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_WordQueryUsageStats_user_profiles_user_id", + table: "WordQueryUsageStats"); + + migrationBuilder.DropPrimaryKey( + name: "PK_WordQueryUsageStats", + table: "WordQueryUsageStats"); + + migrationBuilder.DropPrimaryKey( + name: "PK_SentenceAnalysisCache", + table: "SentenceAnalysisCache"); + + migrationBuilder.RenameTable( + name: "WordQueryUsageStats", + newName: "word_query_usage_stats"); + + migrationBuilder.RenameTable( + name: "SentenceAnalysisCache", + newName: "sentence_analysis_cache"); + + migrationBuilder.AddPrimaryKey( + name: "PK_word_query_usage_stats", + table: "word_query_usage_stats", + column: "id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_sentence_analysis_cache", + table: "sentence_analysis_cache", + column: "id"); + + migrationBuilder.AddForeignKey( + name: "FK_word_query_usage_stats_user_profiles_user_id", + table: "word_query_usage_stats", + column: "user_id", + principalTable: "user_profiles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_word_query_usage_stats_user_profiles_user_id", + table: "word_query_usage_stats"); + + migrationBuilder.DropPrimaryKey( + name: "PK_word_query_usage_stats", + table: "word_query_usage_stats"); + + migrationBuilder.DropPrimaryKey( + name: "PK_sentence_analysis_cache", + table: "sentence_analysis_cache"); + + migrationBuilder.RenameTable( + name: "word_query_usage_stats", + newName: "WordQueryUsageStats"); + + migrationBuilder.RenameTable( + name: "sentence_analysis_cache", + newName: "SentenceAnalysisCache"); + + migrationBuilder.AddPrimaryKey( + name: "PK_WordQueryUsageStats", + table: "WordQueryUsageStats", + column: "id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_SentenceAnalysisCache", + table: "SentenceAnalysisCache", + column: "id"); + + migrationBuilder.AddForeignKey( + name: "FK_WordQueryUsageStats_user_profiles_user_id", + table: "WordQueryUsageStats", + column: "user_id", + principalTable: "user_profiles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/backend/DramaLing.Api/Migrations/DramaLingDbContextModelSnapshot.cs b/backend/DramaLing.Api/Migrations/DramaLingDbContextModelSnapshot.cs index fa59d9f..7382bec 100644 --- a/backend/DramaLing.Api/Migrations/DramaLingDbContextModelSnapshot.cs +++ b/backend/DramaLing.Api/Migrations/DramaLingDbContextModelSnapshot.cs @@ -21,12 +21,14 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("Accent") .IsRequired() .HasMaxLength(2) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("accent"); b.Property("AccessCount") .HasColumnType("INTEGER") @@ -86,7 +88,8 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("AiApiCalls") .HasColumnType("INTEGER") @@ -101,7 +104,8 @@ namespace DramaLing.Api.Migrations .HasColumnName("created_at"); b.Property("Date") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("date"); b.Property("SessionCount") .HasColumnType("INTEGER") @@ -135,7 +139,8 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("AdminNotes") .HasColumnType("TEXT") @@ -146,7 +151,8 @@ namespace DramaLing.Api.Migrations .HasColumnName("created_at"); b.Property("Description") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("description"); b.Property("FlashcardId") .HasColumnType("TEXT") @@ -169,7 +175,8 @@ namespace DramaLing.Api.Migrations b.Property("Status") .IsRequired() .HasMaxLength(50) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("status"); b.Property("StudyMode") .HasMaxLength(50) @@ -195,7 +202,8 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("AccessCount") .HasColumnType("INTEGER") @@ -299,40 +307,30 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("CreatedAt") .HasColumnType("TEXT") .HasColumnName("created_at"); b.Property("Definition") - .IsRequired() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("definition"); b.Property("DifficultyLevel") .HasMaxLength(10) .HasColumnType("TEXT") .HasColumnName("difficulty_level"); - b.Property("EasinessFactor") - .HasColumnType("REAL") - .HasColumnName("easiness_factor"); - b.Property("Example") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("example"); b.Property("ExampleTranslation") .HasColumnType("TEXT") .HasColumnName("example_translation"); - b.Property("FilledQuestionText") - .HasMaxLength(1000) - .HasColumnType("TEXT"); - - b.Property("IntervalDays") - .HasColumnType("INTEGER") - .HasColumnName("interval_days"); - b.Property("IsArchived") .HasColumnType("INTEGER") .HasColumnName("is_archived"); @@ -341,22 +339,6 @@ namespace DramaLing.Api.Migrations .HasColumnType("INTEGER") .HasColumnName("is_favorite"); - b.Property("LastQuestionType") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("LastReviewedAt") - .HasColumnType("TEXT") - .HasColumnName("last_reviewed_at"); - - b.Property("MasteryLevel") - .HasColumnType("INTEGER") - .HasColumnName("mastery_level"); - - b.Property("NextReviewDate") - .HasColumnType("TEXT") - .HasColumnName("next_review_date"); - b.Property("PartOfSpeech") .HasMaxLength(50) .HasColumnType("TEXT") @@ -364,29 +346,13 @@ namespace DramaLing.Api.Migrations b.Property("Pronunciation") .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("Repetitions") - .HasColumnType("INTEGER"); - - b.Property("ReviewHistory") - .HasColumnType("TEXT"); - - b.Property("Synonyms") - .HasMaxLength(2000) - .HasColumnType("TEXT"); - - b.Property("TimesCorrect") - .HasColumnType("INTEGER") - .HasColumnName("times_correct"); - - b.Property("TimesReviewed") - .HasColumnType("INTEGER") - .HasColumnName("times_reviewed"); + .HasColumnType("TEXT") + .HasColumnName("pronunciation"); b.Property("Translation") .IsRequired() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("translation"); b.Property("UpdatedAt") .HasColumnType("TEXT") @@ -399,7 +365,8 @@ namespace DramaLing.Api.Migrations b.Property("Word") .IsRequired() .HasMaxLength(255) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("word"); b.HasKey("Id"); @@ -462,7 +429,8 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("CompletedAt") .HasColumnType("TEXT") @@ -582,7 +550,8 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("CEFRLevel") .IsRequired() @@ -617,7 +586,8 @@ namespace DramaLing.Api.Migrations b.Property("Word") .IsRequired() .HasMaxLength(100) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("word"); b.Property("WordLength") .HasColumnType("INTEGER") @@ -645,7 +615,8 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("AccuracyScore") .HasColumnType("TEXT") @@ -689,10 +660,6 @@ namespace DramaLing.Api.Migrations .HasColumnType("TEXT") .HasColumnName("prosody_score"); - b.Property("StudySessionId") - .HasColumnType("TEXT") - .HasColumnName("study_session_id"); - b.Property("Suggestions") .HasColumnType("TEXT") .HasColumnName("suggestions"); @@ -710,9 +677,6 @@ namespace DramaLing.Api.Migrations b.HasIndex("FlashcardId"); - b.HasIndex("StudySessionId") - .HasDatabaseName("IX_PronunciationAssessment_Session"); - b.HasIndex("UserId", "FlashcardId") .HasDatabaseName("IX_PronunciationAssessment_UserFlashcard"); @@ -723,49 +687,62 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("AccessCount") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("access_count"); b.Property("AnalysisResult") .IsRequired() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("analysis_result"); b.Property("CorrectedText") .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("corrected_text"); b.Property("CreatedAt") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("created_at"); b.Property("ExpiresAt") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("expires_at"); b.Property("GrammarCorrections") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("grammar_corrections"); b.Property("HasGrammarErrors") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("has_grammar_errors"); b.Property("HighValueWords") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("high_value_words"); b.Property("IdiomsDetected") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("idioms_detected"); b.Property("InputText") .IsRequired() .HasMaxLength(1000) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("input_text"); b.Property("InputTextHash") .IsRequired() .HasMaxLength(64) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("input_text_hash"); b.Property("LastAccessedAt") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("last_accessed_at"); b.HasKey("Id"); @@ -778,209 +755,21 @@ namespace DramaLing.Api.Migrations b.HasIndex("InputTextHash", "ExpiresAt") .HasDatabaseName("IX_SentenceAnalysisCache_Hash_Expires"); - b.ToTable("SentenceAnalysisCache"); - }); - - modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("CompletedAt") - .HasColumnType("TEXT"); - - b.Property("FlashcardId") - .HasColumnType("TEXT"); - - b.Property("IsCompleted") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("PlannedTests") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PlannedTestsJson") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("StartedAt") - .HasColumnType("TEXT"); - - b.Property("StudySessionId") - .HasColumnType("TEXT"); - - b.Property("Word") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("FlashcardId"); - - b.HasIndex("StudySessionId"); - - b.ToTable("study_cards", (string)null); - }); - - modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyRecord", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("FlashcardId") - .HasColumnType("TEXT") - .HasColumnName("flashcard_id"); - - b.Property("IsCorrect") - .HasColumnType("INTEGER") - .HasColumnName("is_correct"); - - b.Property("NewEasinessFactor") - .HasColumnType("REAL"); - - b.Property("NewIntervalDays") - .HasColumnType("INTEGER"); - - b.Property("NewRepetitions") - .HasColumnType("INTEGER"); - - b.Property("NextReviewDate") - .HasColumnType("TEXT"); - - b.Property("PreviousEasinessFactor") - .HasColumnType("REAL"); - - b.Property("PreviousIntervalDays") - .HasColumnType("INTEGER"); - - b.Property("PreviousRepetitions") - .HasColumnType("INTEGER"); - - b.Property("QualityRating") - .HasColumnType("INTEGER") - .HasColumnName("quality_rating"); - - b.Property("ResponseTimeMs") - .HasColumnType("INTEGER") - .HasColumnName("response_time_ms"); - - b.Property("SessionId") - .HasColumnType("TEXT") - .HasColumnName("session_id"); - - b.Property("StudiedAt") - .HasColumnType("TEXT") - .HasColumnName("studied_at"); - - b.Property("StudyMode") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT") - .HasColumnName("study_mode"); - - b.Property("UserAnswer") - .HasColumnType("TEXT") - .HasColumnName("user_answer"); - - b.Property("UserId") - .HasColumnType("TEXT") - .HasColumnName("user_id"); - - b.HasKey("Id"); - - b.HasIndex("FlashcardId"); - - b.HasIndex("SessionId"); - - b.HasIndex("UserId", "FlashcardId", "StudyMode") - .IsUnique() - .HasDatabaseName("IX_StudyRecord_UserCard_TestType_Unique"); - - b.ToTable("study_records", (string)null); - }); - - modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("AverageResponseTimeMs") - .HasColumnType("INTEGER") - .HasColumnName("average_response_time_ms"); - - b.Property("CompletedCards") - .HasColumnType("INTEGER"); - - b.Property("CompletedTests") - .HasColumnType("INTEGER"); - - b.Property("CorrectCount") - .HasColumnType("INTEGER") - .HasColumnName("correct_count"); - - b.Property("CurrentCardIndex") - .HasColumnType("INTEGER"); - - b.Property("CurrentTestType") - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("DurationSeconds") - .HasColumnType("INTEGER") - .HasColumnName("duration_seconds"); - - b.Property("EndedAt") - .HasColumnType("TEXT") - .HasColumnName("ended_at"); - - b.Property("SessionType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT") - .HasColumnName("session_type"); - - b.Property("StartedAt") - .HasColumnType("TEXT") - .HasColumnName("started_at"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("TotalCards") - .HasColumnType("INTEGER") - .HasColumnName("total_cards"); - - b.Property("TotalTests") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT") - .HasColumnName("user_id"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("study_sessions", (string)null); + b.ToTable("sentence_analysis_cache", (string)null); }); modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("Color") .IsRequired() .HasMaxLength(50) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("color"); b.Property("CreatedAt") .HasColumnType("TEXT") @@ -989,7 +778,8 @@ namespace DramaLing.Api.Migrations b.Property("Name") .IsRequired() .HasMaxLength(100) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("name"); b.Property("UsageCount") .HasColumnType("INTEGER") @@ -1006,47 +796,12 @@ namespace DramaLing.Api.Migrations b.ToTable("tags", (string)null); }); - modelBuilder.Entity("DramaLing.Api.Models.Entities.TestResult", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("CompletedAt") - .HasColumnType("TEXT"); - - b.Property("ConfidenceLevel") - .HasColumnType("INTEGER"); - - b.Property("IsCorrect") - .HasColumnType("INTEGER"); - - b.Property("ResponseTimeMs") - .HasColumnType("INTEGER"); - - b.Property("StudyCardId") - .HasColumnType("TEXT"); - - b.Property("TestType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("TEXT"); - - b.Property("UserAnswer") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("StudyCardId"); - - b.ToTable("test_results", (string)null); - }); - modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("AvatarUrl") .HasColumnType("TEXT") @@ -1127,7 +882,8 @@ namespace DramaLing.Api.Migrations modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => { b.Property("UserId") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("user_id"); b.Property("AutoPlayEnabled") .HasColumnType("INTEGER") @@ -1180,36 +936,46 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("AutoPlayAudio") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("auto_play_audio"); b.Property("CreatedAt") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("created_at"); b.Property("DailyGoal") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("daily_goal"); b.Property("DifficultyPreference") .IsRequired() .HasMaxLength(20) - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("difficulty_preference"); b.Property("ReminderEnabled") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("reminder_enabled"); b.Property("ReminderTime") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("reminder_time"); b.Property("ShowPronunciation") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("show_pronunciation"); b.Property("UpdatedAt") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("updated_at"); b.Property("UserId") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("user_id"); b.HasKey("Id"); @@ -1223,34 +989,44 @@ namespace DramaLing.Api.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("id"); b.Property("CreatedAt") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("created_at"); b.Property("Date") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("date"); b.Property("HighValueWordClicks") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("high_value_word_clicks"); b.Property("LowValueWordClicks") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("low_value_word_clicks"); b.Property("SentenceAnalysisCount") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("sentence_analysis_count"); b.Property("TotalApiCalls") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("total_api_calls"); b.Property("UniqueWordsQueried") - .HasColumnType("INTEGER"); + .HasColumnType("INTEGER") + .HasColumnName("unique_words_queried"); b.Property("UpdatedAt") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("updated_at"); b.Property("UserId") - .HasColumnType("TEXT"); + .HasColumnType("TEXT") + .HasColumnName("user_id"); b.HasKey("Id"); @@ -1261,7 +1037,7 @@ namespace DramaLing.Api.Migrations .IsUnique() .HasDatabaseName("IX_WordQueryUsageStats_UserDate"); - b.ToTable("WordQueryUsageStats"); + b.ToTable("word_query_usage_stats", (string)null); }); modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b => @@ -1383,11 +1159,6 @@ namespace DramaLing.Api.Migrations .HasForeignKey("FlashcardId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("DramaLing.Api.Models.Entities.StudySession", "StudySession") - .WithMany() - .HasForeignKey("StudySessionId") - .OnDelete(DeleteBehavior.SetNull); - b.HasOne("DramaLing.Api.Models.Entities.User", "User") .WithMany() .HasForeignKey("UserId") @@ -1396,65 +1167,6 @@ namespace DramaLing.Api.Migrations b.Navigation("Flashcard"); - b.Navigation("StudySession"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b => - { - b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") - .WithMany() - .HasForeignKey("FlashcardId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("DramaLing.Api.Models.Entities.StudySession", "StudySession") - .WithMany("StudyCards") - .HasForeignKey("StudySessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Flashcard"); - - b.Navigation("StudySession"); - }); - - modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyRecord", b => - { - b.HasOne("DramaLing.Api.Models.Entities.Flashcard", "Flashcard") - .WithMany("StudyRecords") - .HasForeignKey("FlashcardId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("DramaLing.Api.Models.Entities.StudySession", "Session") - .WithMany("StudyRecords") - .HasForeignKey("SessionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("DramaLing.Api.Models.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Flashcard"); - - b.Navigation("Session"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b => - { - b.HasOne("DramaLing.Api.Models.Entities.User", "User") - .WithMany("StudySessions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - b.Navigation("User"); }); @@ -1469,17 +1181,6 @@ namespace DramaLing.Api.Migrations b.Navigation("User"); }); - modelBuilder.Entity("DramaLing.Api.Models.Entities.TestResult", b => - { - b.HasOne("DramaLing.Api.Models.Entities.StudyCard", "StudyCard") - .WithMany("TestResults") - .HasForeignKey("StudyCardId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("StudyCard"); - }); - modelBuilder.Entity("DramaLing.Api.Models.Entities.UserAudioPreferences", b => { b.HasOne("DramaLing.Api.Models.Entities.User", "User") @@ -1525,20 +1226,6 @@ namespace DramaLing.Api.Migrations b.Navigation("FlashcardExampleImages"); b.Navigation("FlashcardTags"); - - b.Navigation("StudyRecords"); - }); - - modelBuilder.Entity("DramaLing.Api.Models.Entities.StudyCard", b => - { - b.Navigation("TestResults"); - }); - - modelBuilder.Entity("DramaLing.Api.Models.Entities.StudySession", b => - { - b.Navigation("StudyCards"); - - b.Navigation("StudyRecords"); }); modelBuilder.Entity("DramaLing.Api.Models.Entities.Tag", b => @@ -1555,8 +1242,6 @@ namespace DramaLing.Api.Migrations b.Navigation("Flashcards"); b.Navigation("Settings"); - - b.Navigation("StudySessions"); }); #pragma warning restore 612, 618 } diff --git a/backend/DramaLing.Api/Models/Configuration/GeminiOptions.cs b/backend/DramaLing.Api/Models/Configuration/GeminiOptions.cs index 9cae548..57947d9 100644 --- a/backend/DramaLing.Api/Models/Configuration/GeminiOptions.cs +++ b/backend/DramaLing.Api/Models/Configuration/GeminiOptions.cs @@ -9,19 +9,37 @@ public class GeminiOptions [Required(ErrorMessage = "Gemini API Key is required")] public string ApiKey { get; set; } = string.Empty; + /// + /// API 請求超時時間(秒) + /// [Range(1, 120, ErrorMessage = "Timeout must be between 1 and 120 seconds")] public int TimeoutSeconds { get; set; } = 30; + /// + /// API 請求最大重試次數 + /// [Range(1, 10, ErrorMessage = "Max retries must be between 1 and 10")] public int MaxRetries { get; set; } = 3; + /// + /// AI 回應最大 Token 數量 + /// [Range(100, 10000, ErrorMessage = "Max tokens must be between 100 and 10000")] public int MaxOutputTokens { get; set; } = 2000; + /// + /// AI 回應的隨機性程度(0.0-2.0) + /// [Range(0.0, 2.0, ErrorMessage = "Temperature must be between 0 and 2")] public double Temperature { get; set; } = 0.7; - public string Model { get; set; } = "gemini-1.5-flash"; + /// + /// 使用的 Gemini 模型名稱 + /// + public string Model { get; set; } = "gemini-2.0-flash"; + /// + /// Gemini API 基本 URL + /// public string BaseUrl { get; set; } = "https://generativelanguage.googleapis.com"; } \ No newline at end of file diff --git a/backend/DramaLing.Api/Program.cs b/backend/DramaLing.Api/Program.cs index ac7f499..55b4d9b 100644 --- a/backend/DramaLing.Api/Program.cs +++ b/backend/DramaLing.Api/Program.cs @@ -26,17 +26,27 @@ builder.Services.Configure( builder.Configuration.GetSection(ReplicateOptions.SectionName)); builder.Services.AddSingleton, ReplicateOptionsValidator>(); -// 在開發環境設定測試用的API Key -if (builder.Environment.IsDevelopment() && - string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GEMINI_API_KEY"))) +// 在開發環境設定 API Key - 修復配置讀取邏輯 +if (builder.Environment.IsDevelopment()) { builder.Services.PostConfigure(options => { + // 只有當 ApiKey 未設置時才嘗試從其他來源讀取 if (string.IsNullOrEmpty(options.ApiKey)) { - options.ApiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY") - ?? builder.Configuration["Gemini:ApiKey"] - ?? "test-key"; + // 優先從環境變數讀取 + var envApiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"); + if (!string.IsNullOrEmpty(envApiKey)) + { + options.ApiKey = envApiKey; + } + // 如果環境變數沒有,Configuration 應該已經包含 user-secrets + // 這裡只是作為後備,不應該覆蓋已經從 user-secrets 載入的設定 + else if (string.IsNullOrEmpty(builder.Configuration["Gemini:ApiKey"])) + { + // 只有在真的沒有任何配置時才使用測試金鑰 + options.ApiKey = "test-key"; + } } }); } diff --git a/backend/DramaLing.Api/appsettings.json b/backend/DramaLing.Api/appsettings.json index e33ea80..cc80520 100644 --- a/backend/DramaLing.Api/appsettings.json +++ b/backend/DramaLing.Api/appsettings.json @@ -19,15 +19,6 @@ "Frontend": { "Urls": ["http://localhost:3000", "http://localhost:3001"] }, - "Gemini": { - "ApiKey": "", - "TimeoutSeconds": 30, - "MaxRetries": 3, - "MaxOutputTokens": 2000, - "Temperature": 0.7, - "Model": "gemini-1.5-flash", - "BaseUrl": "https://generativelanguage.googleapis.com" - }, "Replicate": { "ApiKey": "", "BaseUrl": "https://api.replicate.com/v1",