mnemo_cards/mnemo_cards_backend/lib/database/daos/test_dao.dart

211 lines
7 KiB
Dart
Raw Normal View History

2025-12-13 13:27:05 +00:00
import 'package:drift/drift.dart';
2025-12-13 23:35:14 +00:00
import 'package:drift_postgres/drift_postgres.dart';
2025-12-13 13:27:05 +00:00
import '../database.dart';
import '../tables/tests.dart';
part 'test_dao.g.dart';
2025-12-20 18:26:15 +00:00
@DriftAccessor(
tables: [Tests, TestQuestions, TestPackRelations, TestStatistics],
)
2025-12-13 13:27:05 +00:00
class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
TestDao(super.db);
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// ==================== Tests ====================
2025-12-20 18:26:15 +00:00
2025-12-14 20:42:38 +00:00
/// Получить тест по ID (только активные)
2025-12-13 20:55:50 +00:00
Future<Test?> getTestById(String id) {
2025-12-14 20:42:38 +00:00
return (select(tests)
2025-12-20 18:26:15 +00:00
..where((t) => t.id.equals(id) & t.isDeleted.equals(false)))
.getSingleOrNull();
2025-12-13 13:27:05 +00:00
}
2025-12-20 18:26:15 +00:00
2025-12-14 20:42:38 +00:00
/// Получить все тесты (только активные)
2025-12-13 13:27:05 +00:00
Future<List<Test>> getAllTests() {
2025-12-20 18:26:15 +00:00
return (select(tests)..where((t) => t.isDeleted.equals(false))).get();
2025-12-13 13:27:05 +00:00
}
2025-12-20 18:26:15 +00:00
2025-12-14 20:42:38 +00:00
/// Получить тесты пака (только активные)
2025-12-13 20:55:50 +00:00
Future<List<Test>> getTestsByPackId(String packId) async {
2025-12-13 13:27:05 +00:00
final query = select(tests).join([
innerJoin(
testPackRelations,
testPackRelations.testId.equalsExp(tests.id) &
2025-12-20 18:26:15 +00:00
testPackRelations.packId.equals(packId),
2025-12-13 13:27:05 +00:00
),
2025-12-20 18:26:15 +00:00
])..where(tests.isDeleted.equals(false));
2025-12-13 13:27:05 +00:00
return query.map((row) => row.readTable(tests)).get();
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Создать тест
2025-12-13 20:55:50 +00:00
Future<String> createTest(TestsCompanion test) async {
final inserted = await into(tests).insertReturning(test);
return inserted.id;
2025-12-13 13:27:05 +00:00
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Обновить тест
Future<bool> updateTest(Test test) {
return update(tests).replace(test);
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Удалить тест (soft delete)
2025-12-13 20:55:50 +00:00
Future<void> softDeleteTest(String testId) {
2025-12-20 18:26:15 +00:00
return (update(tests)..where((t) => t.id.equals(testId))).write(
TestsCompanion(
isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())),
updatedAt: Value(PgDateTime(DateTime.now())),
),
);
2025-12-13 13:27:05 +00:00
}
2025-12-18 20:40:48 +00:00
/// Hard delete old generated tests that were soft-deleted.
///
/// This is important because we use soft delete for regular operations,
/// but generated tests are ephemeral and otherwise will accumulate in DB
/// (along with their questions/stats). Hard delete triggers FK cascades.
Future<int> hardDeleteOldSoftDeletedGeneratedTests({
required Duration olderThan,
}) async {
final threshold = DateTime.now().subtract(olderThan);
2025-12-20 18:26:15 +00:00
final toDelete =
await (select(tests)..where(
(t) =>
t.isDeleted.equals(true) &
t.version.equals('generated') &
t.deletedAt.isSmallerThanValue(PgDateTime(threshold)),
))
.get();
2025-12-18 20:40:48 +00:00
var deleted = 0;
for (final test in toDelete) {
deleted += await (delete(tests)..where((t) => t.id.equals(test.id))).go();
}
return deleted;
}
/// Hard delete generated tests that are not linked to any pack.
///
/// These tests are unreachable from the product (no pack relation) and
/// should not accumulate forever. This also cleans up historical leftovers
/// from the earlier bug where generated tests were created but not linked.
Future<int> hardDeleteOrphanGeneratedTests({
required Duration olderThan,
}) async {
final threshold = DateTime.now().subtract(olderThan);
2025-12-20 18:26:15 +00:00
final rows =
await (select(tests).join([
leftOuterJoin(
testPackRelations,
testPackRelations.testId.equalsExp(tests.id),
),
])..where(
tests.version.equals('generated') &
tests.createdAt.isSmallerThanValue(PgDateTime(threshold)) &
testPackRelations.testId.isNull(),
))
.get();
2025-12-18 20:40:48 +00:00
final orphanTests = rows.map((r) => r.readTable(tests)).toList();
var deleted = 0;
for (final test in orphanTests) {
deleted += await (delete(tests)..where((t) => t.id.equals(test.id))).go();
}
return deleted;
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Связать тест с паком
2025-12-13 20:55:50 +00:00
Future<void> linkTestToPack(String testId, String packId) async {
2025-12-13 13:27:05 +00:00
await into(testPackRelations).insert(
2025-12-20 18:26:15 +00:00
TestPackRelationsCompanion.insert(testId: testId, packId: packId),
2025-12-13 13:27:05 +00:00
mode: InsertMode.insertOrIgnore,
);
}
2025-12-17 00:41:47 +00:00
2025-12-17 19:54:48 +00:00
/// Удалить связь теста с паком
Future<void> unlinkTestFromPack(String testId, String packId) async {
2025-12-20 18:26:15 +00:00
await (delete(testPackRelations)..where(
(tpr) => tpr.testId.equals(testId) & tpr.packId.equals(packId),
))
.go();
2025-12-17 19:54:48 +00:00
}
2025-12-17 00:41:47 +00:00
/// Получить packId для теста
Future<String?> getPackIdForTest(String testId) async {
2025-12-20 18:26:15 +00:00
final relation =
await (select(testPackRelations)
..where((tpr) => tpr.testId.equals(testId))
..limit(1))
.getSingleOrNull();
2025-12-17 00:41:47 +00:00
return relation?.packId;
}
2025-12-17 19:54:48 +00:00
/// Получить все packId для теста
Future<List<String>> getPackIdsForTest(String testId) async {
2025-12-20 18:26:15 +00:00
final relations = await (select(
testPackRelations,
)..where((tpr) => tpr.testId.equals(testId))).get();
2025-12-17 19:54:48 +00:00
return relations.map((r) => r.packId).toList();
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
// ==================== TestQuestions ====================
2025-12-20 18:26:15 +00:00
2025-12-17 00:41:47 +00:00
/// Получить вопросы теста (только активные, отсортированные по orderIndex)
2025-12-13 20:55:50 +00:00
Future<List<TestQuestion>> getTestQuestions(String testId) {
2025-12-13 13:27:05 +00:00
return (select(testQuestions)
2025-12-20 18:26:15 +00:00
..where((tq) => tq.testId.equals(testId) & tq.isDeleted.equals(false))
..orderBy([(tq) => OrderingTerm(expression: tq.orderIndex)]))
.get();
2025-12-13 13:27:05 +00:00
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Создать вопрос теста
2025-12-13 20:55:50 +00:00
Future<String> createTestQuestion(TestQuestionsCompanion question) async {
final inserted = await into(testQuestions).insertReturning(question);
return inserted.id;
2025-12-13 13:27:05 +00:00
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Обновить вопрос теста
Future<bool> updateTestQuestion(TestQuestion question) {
return update(testQuestions).replace(question);
}
2025-12-20 18:26:15 +00:00
2025-12-14 20:42:38 +00:00
/// Удалить вопрос теста (soft delete)
Future<void> softDeleteTestQuestion(String questionId) {
2025-12-20 18:26:15 +00:00
return (update(
testQuestions,
)..where((tq) => tq.id.equals(questionId))).write(
TestQuestionsCompanion(
isDeleted: const Value(true),
deletedAt: Value(PgDateTime(DateTime.now())),
updatedAt: Value(PgDateTime(DateTime.now())),
),
);
}
2025-12-13 13:27:05 +00:00
// ==================== TestStatistics ====================
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Получить статистику теста пользователя
2025-12-13 20:55:50 +00:00
Future<TestStatistic?> getTestStatistics(String userId, String testId) {
2025-12-13 13:27:05 +00:00
return (select(testStatistics)
2025-12-20 18:26:15 +00:00
..where((ts) => ts.userId.equals(userId) & ts.testId.equals(testId)))
.getSingleOrNull();
2025-12-13 13:27:05 +00:00
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Создать статистику теста
2025-12-20 18:26:15 +00:00
Future<String> createTestStatistics(
TestStatisticsCompanion statistics,
) async {
2025-12-13 20:55:50 +00:00
final inserted = await into(testStatistics).insertReturning(statistics);
return inserted.id;
2025-12-13 13:27:05 +00:00
}
2025-12-20 18:26:15 +00:00
2025-12-13 13:27:05 +00:00
/// Обновить статистику теста
Future<bool> updateTestStatistics(TestStatistic statistics) {
return update(testStatistics).replace(statistics);
}
}