mnemo_cards/mnemo_cards_backend/lib/database/daos/pack_dao.dart
Dmitry 4ed1839893
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Web App CI / test (push) Waiting to run
Web App CI / build (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions
format and logs
2026-01-08 16:02:47 +03:00

456 lines
15 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import '../database.dart';
import '../tables/packs.dart';
import '../tables/relations.dart';
part 'pack_dao.g.dart';
@DriftAccessor(
tables: [
CardPacks,
GameCards,
VoiceModels,
PreviewCards,
CardPackCards,
CardVoices,
],
)
class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
PackDao(super.db);
// ==================== CardPacks ====================
/// Получить пак по ID
Future<CardPack?> getPackById(String id) async {
print('🔍 PackDao.getPackById: packId=$id');
print(
'🔍 PackDao.getPackById: SQL: SELECT * FROM card_packs WHERE id = ? LIMIT 1',
);
final query = select(cardPacks)
..where((p) => p.id.equals(id))
..limit(1);
final results = await query.get();
print('✅ PackDao.getPackById: Result count=${results.length}');
return results.isNotEmpty ? results.first : null;
}
/// Получить все паки
Future<List<CardPack>> getAllPacks({
bool enabledOnly = false,
String? orderByField,
bool orderDesc = false,
}) {
final query = select(cardPacks);
if (enabledOnly) {
query.where((p) => p.enabled.equals(true));
}
query.where((p) => p.isDeleted.equals(false));
if (orderByField == 'order') {
if (orderDesc) {
query.orderBy([(p) => OrderingTerm.desc(p.order)]);
} else {
query.orderBy([(p) => OrderingTerm.asc(p.order)]);
}
} else {
query.orderBy([(p) => OrderingTerm.asc(p.order)]);
}
return query.get();
}
/// Создать пак
Future<String> createPack(CardPacksCompanion pack) async {
final inserted = await into(cardPacks).insertReturning(pack);
return inserted.id;
}
/// Обновить пак
Future<bool> updatePack(CardPack pack) {
return update(cardPacks).replace(pack);
}
/// Обновить пак частично
Future<void> updatePackPartial(CardPacksCompanion updates) {
if (!updates.id.present) throw ArgumentError('Pack ID is required');
final packId = updates.id.value;
return (update(cardPacks)..where((p) => p.id.equals(packId))).write(
updates.copyWith(updatedAt: Value(PgDateTime(DateTime.now()))),
);
}
/// Удалить пак (soft delete)
Future<void> softDeletePack(String packId) {
return (update(cardPacks)..where((p) => p.id.equals(packId))).write(
CardPacksCompanion(
isDeleted: const Value(true),
updatedAt: Value(PgDateTime(DateTime.now())),
),
);
}
/// Подсчитать паки
Future<int> countPacks({bool enabledOnly = false}) async {
final countExpr = cardPacks.id.count();
final query = selectOnly(cardPacks)..addColumns([countExpr]);
if (enabledOnly) {
query.where(cardPacks.enabled.equals(true));
}
query.where(cardPacks.isDeleted.equals(false));
return await query.map((row) => row.read(countExpr)!).getSingle();
}
// ==================== GameCards ====================
/// Получить карточку по ID (только активные)
Future<GameCard?> getCardById(String id) {
return (select(gameCards)
..where((c) => c.id.equals(id) & c.isDeleted.equals(false)))
.getSingleOrNull();
}
/// Получить паки для карточки
/// Связь через CardPackCards (packId удален из GameCards)
Future<List<CardPack>> getPacksForCard(String cardId) async {
final query =
select(cardPacks).join([
innerJoin(
cardPackCards,
cardPackCards.packId.equalsExp(cardPacks.id),
),
])..where(
cardPackCards.cardId.equals(cardId) &
cardPacks.isDeleted.equals(false),
);
final rows = await query.get();
return rows.map((row) => row.readTable(cardPacks)).toList();
}
/// Получить все карточки пака
/// Связь теперь только через CardPackCards (packId удален из GameCards)
Future<List<GameCard>> getPackCards(String packId) async {
final query = select(gameCards).join([
innerJoin(
cardPackCards,
cardPackCards.cardId.equalsExp(gameCards.id) &
cardPackCards.packId.equals(packId),
),
])..where(gameCards.isDeleted.equals(false));
final results = await query.get();
// Сортируем по order из junction table, если есть, иначе по ID
results.sort((a, b) {
final orderA = a.read(cardPackCards.order) ?? 999999;
final orderB = a.read(cardPackCards.order) ?? 999999;
if (orderA != orderB) return orderA.compareTo(orderB);
return a.readTable(gameCards).id.compareTo(b.readTable(gameCards).id);
});
return results.map((row) => row.readTable(gameCards)).toList();
}
/// Получить карточки по списку ID
Future<List<GameCard>> getCardsByIds(List<String> ids) {
if (ids.isEmpty) return Future.value([]);
return (select(gameCards)..where((c) => c.id.isIn(ids))).get();
}
/// Найти карточки по original (слову)
/// Возвращает все карточки с указанным original (может быть несколько)
Future<List<GameCard>> searchCardsByOriginal(String original) {
return (select(gameCards)
..where(
(c) => c.original.equals(original) & c.isDeleted.equals(false),
)
..limit(10) // Ограничиваем для производительности
)
.get();
}
/// Создать карточку
Future<String> createCard(GameCardsCompanion card) async {
final inserted = await into(gameCards).insertReturning(card);
return inserted.id;
}
/// Обновить карточку
Future<bool> updateCard(GameCard card) {
return update(gameCards).replace(card);
}
/// Удалить карточку (soft delete)
Future<void> softDeleteCard(String cardId) {
return (update(gameCards)..where((c) => c.id.equals(cardId))).write(
GameCardsCompanion(
isDeleted: const Value(true),
updatedAt: Value(PgDateTime(DateTime.now())),
),
);
}
/// Удалить карточку (hard delete)
Future<void> deleteCard(String cardId) {
return (delete(gameCards)..where((c) => c.id.equals(cardId))).go();
}
/// Получить все карточки с пагинацией
Future<List<GameCard>> getAllCards({int? limit, int? offset}) {
final query = select(gameCards);
if (limit != null) {
query.limit(limit, offset: offset);
}
return query.get();
}
/// Подсчитать карточки
Future<int> countCards() async {
final countExpr = gameCards.id.count();
final query = selectOnly(gameCards)..addColumns([countExpr]);
return await query.map((row) => row.read(countExpr)!).getSingle();
}
/// Добавить карточку в пак
/// Если связь уже существует, метод завершится успешно без ошибки
Future<void> addCardToPack({
required String packId,
required String cardId,
int order = 0,
}) async {
// Проверить, существует ли уже связь
final existing =
await (select(cardPackCards)..where(
(cpc) => cpc.packId.equals(packId) & cpc.cardId.equals(cardId),
))
.getSingleOrNull();
// Если связь уже существует, ничего не делаем
if (existing != null) {
return;
}
// Иначе создаем новую связь
await into(cardPackCards).insert(
CardPackCardsCompanion.insert(
packId: packId,
cardId: cardId,
order: Value(order),
),
);
}
/// Удалить карточку из пака
Future<void> removeCardFromPack(String packId, String cardId) async {
await (delete(cardPackCards)..where(
(cpc) => cpc.packId.equals(packId) & cpc.cardId.equals(cardId),
))
.go();
}
/// Обновить порядок карточек в паке
Future<void> updatePackCardsOrder(String packId, List<String> cardIds) async {
await transaction(() async {
// Удалить старые связи
await (delete(
cardPackCards,
)..where((cpc) => cpc.packId.equals(packId))).go();
// Создать новые связи с порядком
for (var i = 0; i < cardIds.length; i++) {
await into(cardPackCards).insert(
CardPackCardsCompanion.insert(
packId: packId,
cardId: cardIds[i],
order: Value(i),
),
);
}
});
}
// ==================== PreviewCards ====================
/// Получить preview карточки пака
Future<List<GameCard>> getPreviewCards(String packId) async {
final query = select(gameCards).join([
innerJoin(
db.previewCards,
db.previewCards.cardId.equalsExp(gameCards.id) &
db.previewCards.packId.equals(packId),
),
]);
final results = await query.get();
// Сортируем вручную по order из junction table
results.sort((a, b) {
final orderA = a.read(db.previewCards.order) ?? 0;
final orderB = b.read(db.previewCards.order) ?? 0;
return orderA.compareTo(orderB);
});
return results.map((row) => row.readTable(gameCards)).toList();
}
/// Установить preview карточки для пака
Future<void> setPreviewCards(String packId, List<String> cardIds) async {
await transaction(() async {
// Удалить старые preview карточки
await (delete(
previewCards,
)..where((pc) => pc.packId.equals(packId))).go();
// Добавить новые
for (var i = 0; i < cardIds.length; i++) {
await into(previewCards).insert(
PreviewCardsCompanion.insert(
packId: packId,
cardId: cardIds[i],
order: Value(i),
),
);
}
});
}
// ==================== VoiceModels ====================
/// Получить голосовые модели карточки
Future<List<VoiceModel>> getCardVoices(String cardId) async {
print('🔍 DAO getCardVoices: Querying voices for card $cardId');
// First, check if there are any voices in VoiceModels table with direct cardId link
final directVoicesQuery = select(voiceModels)
..where((v) => v.cardId.equals(cardId));
final directVoices = await directVoicesQuery.get();
print(
'🔍 DAO getCardVoices: Found ${directVoices.length} voices in VoiceModels with direct cardId link',
);
// Check if there are any voices in CardVoices table for this card
final countExpr = cardVoices.cardId.count();
final countQuery = selectOnly(cardVoices)
..addColumns([countExpr])
..where(cardVoices.cardId.equals(cardId));
final countResult = await countQuery.getSingle();
final count = countResult.read(countExpr) ?? 0;
print(
'🔍 DAO getCardVoices: Found $count entries in CardVoices junction table for card $cardId',
);
// Query through junction table
final query = select(voiceModels).join([
innerJoin(
cardVoices,
cardVoices.voiceId.equalsExp(voiceModels.id) &
cardVoices.cardId.equals(cardId),
),
]);
final result = await query.map((row) => row.readTable(voiceModels)).get();
print(
'🔍 DAO getCardVoices: Query through junction table returned ${result.length} voices',
);
// If junction table query returns nothing but direct link exists, return direct voices
if (result.isEmpty && directVoices.isNotEmpty) {
print(
'⚠️ DAO getCardVoices: Junction table is empty but direct link exists! Using direct voices.',
);
return directVoices;
}
return result;
}
/// Добавить голосовую модель к карточке
Future<void> addVoiceToCard(String cardId, String voiceId) async {
print('🔍 DAO addVoiceToCard: Linking voice $voiceId to card $cardId');
try {
// Check if voice exists
final voice = await getVoiceById(voiceId);
if (voice == null) {
print('❌ DAO addVoiceToCard: Voice $voiceId does not exist!');
throw Exception('Voice $voiceId does not exist');
}
print(
'✅ DAO addVoiceToCard: Voice $voiceId exists with cardId=${voice.cardId}',
);
// Check if card exists
final card = await getCardById(cardId);
if (card == null) {
print('❌ DAO addVoiceToCard: Card $cardId does not exist!');
throw Exception('Card $cardId does not exist');
}
print('✅ DAO addVoiceToCard: Card $cardId exists');
// Insert into junction table
final result = await into(cardVoices).insert(
CardVoicesCompanion.insert(cardId: cardId, voiceId: voiceId),
mode: InsertMode.insertOrIgnore,
);
print('✅ DAO addVoiceToCard: Insert result: $result');
// Verify the insert
final verifyQuery = select(cardVoices)
..where((cv) => cv.cardId.equals(cardId) & cv.voiceId.equals(voiceId));
final verifyResult = await verifyQuery.getSingleOrNull();
if (verifyResult == null) {
print(
'❌ DAO addVoiceToCard: Verification failed! Link was not created.',
);
} else {
print(
'✅ DAO addVoiceToCard: Successfully linked and verified voice $voiceId to card $cardId',
);
}
} catch (e, stackTrace) {
print(
'❌ DAO addVoiceToCard: Error linking voice $voiceId to card $cardId: $e',
);
print('Stack trace: $stackTrace');
rethrow;
}
}
/// Удалить голосовую модель из карточки
Future<void> removeVoiceFromCard(String cardId, String voiceId) async {
await (delete(
cardVoices,
)..where((cv) => cv.cardId.equals(cardId) & cv.voiceId.equals(voiceId)))
.go();
}
/// Создать голосовую модель
Future<String> createVoice(VoiceModelsCompanion voice) async {
final inserted = await into(voiceModels).insertReturning(voice);
return inserted.id;
}
/// Получить голосовую модель по ID
Future<VoiceModel?> getVoiceById(String id) {
return (select(
voiceModels,
)..where((v) => v.id.equals(id))).getSingleOrNull();
}
/// Обновить путь/URL аудиофайла голосовой модели
Future<void> updateVoiceUrl(String voiceId, String voiceUrl) async {
await (update(voiceModels)..where((v) => v.id.equals(voiceId))).write(
VoiceModelsCompanion(voiceUrl: Value(voiceUrl)),
);
}
/// Удалить голосовую модель
Future<void> deleteVoice(String voiceId) async {
await (delete(voiceModels)..where((v) => v.id.equals(voiceId))).go();
}
}