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

208 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';
@DriftAccessor(tables: [Tests, TestQuestions, TestPackRelations, TestStatistics])
class TestDao extends DatabaseAccessor<AppDatabase> with _$TestDaoMixin {
TestDao(super.db);
// ==================== Tests ====================
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)
..where((t) => t.id.equals(id) & t.isDeleted.equals(false))
).getSingleOrNull();
2025-12-13 13:27:05 +00:00
}
2025-12-14 20:42:38 +00:00
/// Получить все тесты (только активные)
2025-12-13 13:27:05 +00:00
Future<List<Test>> getAllTests() {
return (select(tests)
..where((t) => t.isDeleted.equals(false))
).get();
}
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) &
testPackRelations.packId.equals(packId),
),
2025-12-14 20:42:38 +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-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
}
/// Обновить тест
Future<bool> updateTest(Test test) {
return update(tests).replace(test);
}
/// Удалить тест (soft delete)
2025-12-13 20:55:50 +00:00
Future<void> softDeleteTest(String testId) {
2025-12-13 13:27:05 +00:00
return (update(tests)..where((t) => t.id.equals(testId)))
.write(TestsCompanion(
isDeleted: const Value(true),
2025-12-17 00:41:47 +00:00
deletedAt: Value(PgDateTime(DateTime.now())),
2025-12-13 23:35:14 +00:00
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);
final toDelete = await (select(tests)
..where(
(t) =>
t.isDeleted.equals(true) &
t.version.equals('generated') &
t.deletedAt.isSmallerThanValue(PgDateTime(threshold)),
))
.get();
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);
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();
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-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(
TestPackRelationsCompanion.insert(
testId: testId,
packId: packId,
),
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 {
await (delete(testPackRelations)
..where((tpr) => tpr.testId.equals(testId) & tpr.packId.equals(packId))
).go();
}
2025-12-17 00:41:47 +00:00
/// Получить packId для теста
Future<String?> getPackIdForTest(String testId) async {
final relation = await (select(testPackRelations)
..where((tpr) => tpr.testId.equals(testId))
..limit(1)
).getSingleOrNull();
return relation?.packId;
}
2025-12-17 19:54:48 +00:00
/// Получить все packId для теста
Future<List<String>> getPackIdsForTest(String testId) async {
final relations = await (select(testPackRelations)
..where((tpr) => tpr.testId.equals(testId))
).get();
return relations.map((r) => r.packId).toList();
}
2025-12-13 13:27:05 +00:00
// ==================== TestQuestions ====================
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-14 20:42:38 +00:00
..where((tq) => tq.testId.equals(testId) & tq.isDeleted.equals(false))
2025-12-17 00:41:47 +00:00
..orderBy([(tq) => OrderingTerm(expression: tq.orderIndex)])
2025-12-13 13:27:05 +00:00
).get();
}
/// Создать вопрос теста
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
}
/// Обновить вопрос теста
Future<bool> updateTestQuestion(TestQuestion question) {
return update(testQuestions).replace(question);
}
2025-12-14 20:42:38 +00:00
/// Удалить вопрос теста (soft delete)
Future<void> softDeleteTestQuestion(String questionId) {
return (update(testQuestions)..where((tq) => tq.id.equals(questionId)))
.write(TestQuestionsCompanion(
isDeleted: const Value(true),
2025-12-14 21:10:06 +00:00
deletedAt: Value(PgDateTime(DateTime.now())),
2025-12-14 20:42:38 +00:00
updatedAt: Value(PgDateTime(DateTime.now())),
));
2025-12-13 13:27:05 +00:00
}
// ==================== TestStatistics ====================
/// Получить статистику теста пользователя
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)
..where((ts) => ts.userId.equals(userId) & ts.testId.equals(testId))
).getSingleOrNull();
}
/// Создать статистику теста
2025-12-13 20:55:50 +00:00
Future<String> createTestStatistics(TestStatisticsCompanion statistics) async {
final inserted = await into(testStatistics).insertReturning(statistics);
return inserted.id;
2025-12-13 13:27:05 +00:00
}
/// Обновить статистику теста
Future<bool> updateTestStatistics(TestStatistic statistics) {
return update(testStatistics).replace(statistics);
}
}