using LiteDB; using LehrerApp.Core.Models; namespace LehrerApp.Data; /// /// Zentrale LiteDB-Verbindung. Singleton – eine Datei = ein Nutzer. /// public class LiteDbContext : IDisposable { private readonly LiteDatabase _db; public LiteDbContext(string databasePath) { _db = new LiteDatabase(new ConnectionString(databasePath) { Connection = ConnectionType.Shared, }); MigrateExistingData(); EnsureIndexes(); } public ILiteCollection Students => _db.GetCollection("students"); public ILiteCollection Groups => _db.GetCollection("groups"); public ILiteCollection Memberships => _db.GetCollection("group_memberships"); public ILiteCollection Exams => _db.GetCollection("exams"); public ILiteCollection ExamResults => _db.GetCollection("exam_results"); public ILiteCollection Grades => _db.GetCollection("grades"); public ILiteCollection GradingSchemes => _db.GetCollection("grading_schemes"); public ILiteCollection ReportGrades => _db.GetCollection("report_grades"); public ILiteCollection GradingKeyTemplates => _db.GetCollection("grading_key_templates"); public ILiteCollection Units => _db.GetCollection("units"); public ILiteCollection Lessons => _db.GetCollection("lessons"); public ILiteCollection Documentation => _db.GetCollection("documentation"); public ILiteCollection Tasks => _db.GetCollection("tasks"); public ILiteCollection TimeEntries => _db.GetCollection("time_entries"); public ILiteCollection ParticipationSessions => _db.GetCollection("participation_sessions"); public ILiteCollection ParticipationEntries => _db.GetCollection("participation"); public ILiteCollection ParticipationAspects => _db.GetCollection("participation_aspects"); public ILiteCollection ParticipationSections => _db.GetCollection("participation_sections"); public ILiteCollection Subjects => _db.GetCollection("subjects"); public ILiteCollection CompetencyDomains => _db.GetCollection("competency_domains"); public void Checkpoint() => _db.Checkpoint(); private void MigrateExistingData() { MigrateMemberships(); RemoveRedundantLegacyFields(); var groups = _db.GetCollection("groups"); var subjects = _db.GetCollection("subjects"); var missingArchiveState = groups.FindAll() .Where(g => !g.ContainsKey(nameof(LearningGroup.IsActive))) .ToList(); foreach (var group in missingArchiveState) { group[nameof(LearningGroup.IsActive)] = true; groups.Update(group); } // Subject war anfangs nur als Text in der Gruppe gespeichert. Alte // Werte werden einmalig an die Fachstammdaten angebunden und danach // entfernt, damit es nur noch eine führende Fachbezeichnung gibt. foreach (var group in groups.FindAll().ToList()) { var changed = false; var legacyName = group.TryGetValue("Subject", out var value) && value.IsString ? value.AsString.Trim() : ""; var hasSubjectId = group.TryGetValue(nameof(LearningGroup.SubjectId), out var id) && id.IsGuid && subjects.FindById(id.AsGuid) is not null; if (!hasSubjectId && legacyName.Length > 0) { var subject = subjects.FindAll().FirstOrDefault(s => string.Equals(s.Name.Trim(), legacyName, StringComparison.OrdinalIgnoreCase)); if (subject is null) { subject = new Subject { Name = legacyName }; subjects.Insert(subject); } group[nameof(LearningGroup.SubjectId)] = subject.Id; changed = true; } else if (!hasSubjectId && group.ContainsKey(nameof(LearningGroup.SubjectId))) { group[nameof(LearningGroup.SubjectId)] = BsonValue.Null; changed = true; } changed |= group.Remove("Subject"); if (changed) groups.Update(group); } } private void MigrateMemberships() { const string oldName = "enrollments"; const string newName = "group_memberships"; var names = _db.GetCollectionNames().ToHashSet(StringComparer.OrdinalIgnoreCase); if (names.Contains(oldName) && !names.Contains(newName)) _db.RenameCollection(oldName, newName); var memberships = _db.GetCollection(newName); foreach (var membership in memberships.FindAll().ToList()) { var changed = false; if (!membership.ContainsKey(nameof(GroupMembership.AddedOn)) && membership.TryGetValue("EnrolledAt", out var enrolledAt)) { membership[nameof(GroupMembership.AddedOn)] = enrolledAt; changed = true; } changed |= membership.Remove("EnrolledAt"); changed |= membership.Remove("SchoolYear"); if (changed) memberships.Update(membership); } } private void RemoveRedundantLegacyFields() { RemoveFields("exams", "Subject"); RemoveFields("units", "Subject", "SchoolYear"); RemoveFields("grades", "SchoolYear"); RemoveFields("participation", "GroupId", "Date"); } private void RemoveFields(string collectionName, params string[] fieldNames) { var collection = _db.GetCollection(collectionName); foreach (var document in collection.FindAll().ToList()) { var changed = false; foreach (var fieldName in fieldNames) changed |= document.Remove(fieldName); if (changed) collection.Update(document); } } private void EnsureIndexes() { Students.EnsureIndex(x => x.LastName); Students.EnsureIndex(x => x.IsActive); Groups.EnsureIndex(x => x.SchoolYear); Groups.EnsureIndex(x => x.IsActive); Memberships.EnsureIndex(x => x.StudentId); Memberships.EnsureIndex(x => x.GroupId); Memberships.EnsureIndex("ux_student_group", BsonExpression.Create("STRING($.StudentId) + ':' + STRING($.GroupId)"), unique: true); Exams.EnsureIndex(x => x.GroupId); Exams.EnsureIndex(x => x.Status); ExamResults.EnsureIndex(x => x.ExamId); ExamResults.EnsureIndex(x => x.StudentId); ExamResults.EnsureIndex("ux_exam_student", BsonExpression.Create("STRING($.ExamId) + ':' + STRING($.StudentId)"), unique: true); Grades.EnsureIndex(x => x.StudentId); Grades.EnsureIndex(x => x.GroupId); GradingSchemes.EnsureIndex(x => x.GroupId); GradingSchemes.EnsureIndex(x => x.GroupType); ReportGrades.EnsureIndex(x => x.GroupId); ReportGrades.EnsureIndex(x => x.StudentId); ReportGrades.EnsureIndex("ux_student_group_period", BsonExpression.Create("STRING($.StudentId) + ':' + STRING($.GroupId) + ':' + $.Period"), unique: true); GradingKeyTemplates.EnsureIndex(x => x.GradingSystem); Units.EnsureIndex(x => x.GroupId); Lessons.EnsureIndex(x => x.UnitId); Lessons.EnsureIndex(x => x.GroupId); Lessons.EnsureIndex(x => x.Date); Documentation.EnsureIndex(x => x.StudentId); Tasks.EnsureIndex(x => x.Status); TimeEntries.EnsureIndex(x => x.Date); ParticipationSessions.EnsureIndex(x => x.GroupId); ParticipationSessions.EnsureIndex(x => x.Date); ParticipationEntries.EnsureIndex(x => x.SessionId); ParticipationEntries.EnsureIndex(x => x.StudentId); ParticipationEntries.EnsureIndex("ux_session_student", BsonExpression.Create("STRING($.SessionId) + ':' + STRING($.StudentId)"), unique: true); ParticipationAspects.EnsureIndex(x => x.GroupId); ParticipationSections.EnsureIndex(x => x.GroupId); Subjects.EnsureIndex(x => x.Name); Subjects.EnsureIndex("ux_subject_name", BsonExpression.Create("LOWER(TRIM($.Name))"), unique: true); CompetencyDomains.EnsureIndex(x => x.SubjectId); CompetencyDomains.EnsureIndex(x => x.GradeLevel); } public void Dispose() => _db.Dispose(); }