import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:okane/ui/utils.dart'; import 'package:path_provider/path_provider.dart'; part 'sqlite.g.dart'; class Accounts extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text()(); } enum BeneficiaryType { account, other } class Beneficiaries extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text().unique()(); TextColumn get type => textEnum()(); IntColumn get accountId => integer().nullable().references(Accounts, #id)(); TextColumn get imagePath => text().nullable()(); } class ExpenseCategories extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text()(); } enum BudgetPeriod { month } class BudgetItems extends Table { IntColumn get id => integer().autoIncrement()(); RealColumn get amount => real()(); IntColumn get expenseCategoryId => integer().references(ExpenseCategories, #id)(); IntColumn get budgetId => integer().references(Budgets, #id)(); } class BudgetItemDto { final BudgetItem item; final ExpenseCategory expenseCategory; BudgetItemDto({required this.item, required this.expenseCategory}); } class Budgets extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get period => textEnum()(); TextColumn get name => text()(); RealColumn get income => real()(); BoolColumn get includeOtherSpendings => boolean()(); IntColumn get accountId => integer().references(Accounts, #id)(); } class BudgetsDto { final Budget budget; BudgetsDto({required this.budget}); } class Loans extends Table { IntColumn get id => integer().autoIncrement()(); IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)(); } class LoanChanges extends Table { IntColumn get id => integer().autoIncrement()(); IntColumn get loanId => integer().references(Loans, #id)(); RealColumn get amount => real()(); DateTimeColumn get date => dateTime()(); } class LoanDto { final Loan loan; final Beneficiary beneficiary; LoanDto({required this.loan, required this.beneficiary}); } class RecurringTransactions extends Table { IntColumn get id => integer().autoIncrement()(); IntColumn get days => integer()(); DateTimeColumn get lastExecution => dateTime().nullable()(); IntColumn get templateId => integer().references(TransactionTemplates, #id)(); IntColumn get accountId => integer().references(Accounts, #id)(); } typedef RecurringTransactionDto = ({ RecurringTransaction recurring, Beneficiary beneficiary, TransactionTemplate template, }); class TransactionTemplates extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get name => text()(); RealColumn get amount => real()(); BoolColumn get recurring => boolean()(); IntColumn get expenseCategoryId => integer().nullable().references(ExpenseCategories, #id)(); IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)(); IntColumn get accountId => integer().references(Accounts, #id)(); } typedef TransactionTemplateDto = ({ TransactionTemplate template, Beneficiary beneficiary, ExpenseCategory? expenseCategory, }); class Transactions extends Table { IntColumn get id => integer().autoIncrement()(); RealColumn get amount => real()(); // TODO: tags DateTimeColumn get date => dateTime()(); IntColumn get expenseCategoryId => integer().nullable().references(ExpenseCategories, #id)(); IntColumn get accountId => integer().references(Accounts, #id)(); IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)(); } class TransactionDto { final Transaction transaction; final Beneficiary beneficiary; final ExpenseCategory? expenseCategory; TransactionDto({ required this.transaction, required this.beneficiary, required this.expenseCategory, }); } @DriftDatabase( tables: [ Accounts, Beneficiaries, Budgets, BudgetItems, ExpenseCategories, Loans, LoanChanges, RecurringTransactions, TransactionTemplates, Transactions, ], daos: [ AccountsDao, BeneficiariesDao, BudgetsDao, ExpenseCategoriesDao, LoansDao, RecurringTransactionsDao, TransactionTemplatesDao, TransactionsDao, ], ) class OkaneDatabase extends _$OkaneDatabase { OkaneDatabase() : super(_openConnection()); @override int get schemaVersion => 1; static QueryExecutor _openConnection() { return driftDatabase( name: "okane", native: const DriftNativeOptions( databaseDirectory: getApplicationSupportDirectory, ), ); } } @DriftAccessor(tables: [Accounts]) class AccountsDao extends DatabaseAccessor with _$AccountsDaoMixin { AccountsDao(OkaneDatabase db) : super(db); Stream> accountsStream() { return select(accounts).watch(); } Future> getAccounts() { return select(accounts).get(); } Future upsertAccount(AccountsCompanion account) { return into(accounts).insertOnConflictUpdate(account); } } enum TransactionQueryDateOption { thisMonth } @DriftAccessor(tables: [Transactions, Beneficiaries, ExpenseCategories]) class TransactionsDao extends DatabaseAccessor with _$TransactionsDaoMixin { TransactionsDao(OkaneDatabase db) : super(db); JoinedSelectStatement _transactionQuery(Account account) { return (select(transactions) ..where((t) => t.accountId.equals(account.id))).join([ leftOuterJoin( beneficiaries, beneficiaries.id.equalsExp(transactions.beneficiaryId), ), leftOuterJoin( expenseCategories, expenseCategories.id.equalsExp(transactions.expenseCategoryId), ), ]); } TransactionDto _mapToDto(TypedResult row) { return TransactionDto( transaction: row.readTable(transactions), beneficiary: row.readTable(beneficiaries), expenseCategory: row.readTableOrNull(expenseCategories), ); } Stream> transactionsStream(Account account) { return _transactionQuery(account).watch().map((rows) { return rows.map(_mapToDto).toList(); }); } Future> getTransactions(Account? account) { if (account == null) { return Future.value(List.empty()); } return _transactionQuery( account, ).get().then((rows) => rows.map(_mapToDto).toList()); } Future> getLastTransactions( Account account, DateTime today, int days, ) async { return (select(transactions)..where( (t) => t.accountId.equals(account.id) & t.date.isBiggerThanValue( toMidnight(today.subtract(Duration(days: days))), ), )) .join([ leftOuterJoin( beneficiaries, beneficiaries.id.equalsExp(transactions.beneficiaryId), ), leftOuterJoin( expenseCategories, expenseCategories.id.equalsExp(transactions.expenseCategoryId), ), ]) .get() .then((rows) => rows.map(_mapToDto).toList()); } Future getTotalBalance(Iterable accountIds) async { final sum = transactions.amount.sum(); final query = selectOnly(transactions) ..where(transactions.accountId.isIn(accountIds)) ..addColumns([sum]); return query .map((row) => row.read(sum)) .getSingleOrNull() .then((v) => v ?? 0); } Future> getTransactionsInTimeframe( Account account, DateTime today, TransactionQueryDateOption option, ) { final lower = switch (option) { TransactionQueryDateOption.thisMonth => DateTime( today.year, today.month, 0, ), }; final upper = switch (option) { TransactionQueryDateOption.thisMonth => monthEnding(today), }; return (select(transactions)..where( (t) => t.accountId.equals(account.id) & t.date.isBetweenValues(lower, upper), )) .join([ leftOuterJoin( beneficiaries, beneficiaries.id.equalsExp(transactions.beneficiaryId), ), leftOuterJoin( expenseCategories, expenseCategories.id.equalsExp(transactions.expenseCategoryId), ), ]) .get() .then((rows) => rows.map(_mapToDto).toList()); } Future upsertTransaction(TransactionsCompanion t) { return into( transactions, ).insertReturning(t, mode: InsertMode.insertOrReplace); } } @DriftAccessor(tables: [Beneficiaries]) class BeneficiariesDao extends DatabaseAccessor with _$BeneficiariesDaoMixin { BeneficiariesDao(OkaneDatabase db) : super(db); Stream> beneficiariesStream() { return select(beneficiaries).watch(); } Future> getBeneficiaries() { return select(beneficiaries).get(); } Future upsertBeneficiary(BeneficiariesCompanion beneficiary) { return into( beneficiaries, ).insertReturning(beneficiary, mode: InsertMode.insertOrReplace); } Future getAccountBeneficiary(Account account) { return (select(beneficiaries) ..where((b) => b.accountId.equals(account.id))).getSingle(); } Stream watchBeneficiary(int id) { return (select(beneficiaries)..where((b) => b.id.equals(id))).watchSingle(); } } @DriftAccessor(tables: [ExpenseCategories]) class ExpenseCategoriesDao extends DatabaseAccessor with _$ExpenseCategoriesDaoMixin { ExpenseCategoriesDao(OkaneDatabase db) : super(db); Stream> expenseCategoriesStream(Account account) { return select(expenseCategories).watch(); } Future> getExpenseCategories(Account? account) { if (account == null) { return Future.value(List.empty()); } return select(expenseCategories).get(); } Future upsertCategory(ExpenseCategoriesCompanion category) { return into( expenseCategories, ).insertReturning(category, mode: InsertMode.insertOrReplace); } } @DriftAccessor(tables: [Budgets, BudgetItems]) class BudgetsDao extends DatabaseAccessor with _$BudgetsDaoMixin { BudgetsDao(OkaneDatabase db) : super(db); Stream> budgetsStream(Account account) { return (select(budgets) ..where((b) => b.accountId.equals(account.id))).watch().map((rows) { return rows.map((row) { return BudgetsDto(budget: row); }).toList(); }); } Future> getBudgets(Account? account) { if (account == null) { return Future.value(List.empty()); } return (select(budgets) ..where((b) => b.accountId.equals(account.id))).get().then((rows) { return rows.map((row) { return BudgetsDto(budget: row); }).toList(); }); } Stream> watchBudgetItems(Budget budget) { return (select(budgetItems)..where((b) => b.budgetId.equals(budget.id))) .join([ leftOuterJoin( expenseCategories, expenseCategories.id.equalsExp(budgetItems.expenseCategoryId), ), ]) .watch() .map((rows) { return rows.map((row) { return BudgetItemDto( expenseCategory: row.readTable(expenseCategories), item: row.readTable(budgetItems), ); }).toList(); }); } Future upsertBudget(BudgetsCompanion budget) { return into( budgets, ).insertReturning(budget, mode: InsertMode.insertOrReplace); } Future upsertBudgetItem(BudgetItemsCompanion item) { return into( budgetItems, ).insertReturning(item, mode: InsertMode.insertOrReplace); } } @DriftAccessor(tables: [Loans, LoanChanges, Beneficiaries]) class LoansDao extends DatabaseAccessor with _$LoansDaoMixin { LoansDao(OkaneDatabase db) : super(db); Stream> loansStream(Account account) { return select(loans) .join([ leftOuterJoin( beneficiaries, beneficiaries.id.equalsExp(loans.beneficiaryId), ), ]) .watch() .map((rows) { return rows.map((row) { return LoanDto( loan: row.readTable(loans), beneficiary: row.readTable(beneficiaries), ); }).toList(); }); } Future> getLoans(Account? account) { if (account == null) { return Future.value(List.empty()); } return select(loans) .join([ leftOuterJoin( beneficiaries, beneficiaries.id.equalsExp(loans.beneficiaryId), ), ]) .get() .then((rows) { return rows.map((row) { return LoanDto( loan: row.readTable(loans), beneficiary: row.readTable(beneficiaries), ); }).toList(); }); } Future getTotalLoanSum() async { final count = loanChanges.amount.sum(); final query = selectOnly(loanChanges)..addColumns([count]); return query .map((row) => row.read(count)) .getSingleOrNull() .then((v) => v ?? 0); } Future upsertLoan(LoansCompanion loan) { return into(loans).insertReturning(loan, mode: InsertMode.insertOrReplace); } Future upsertLoanChange(LoanChangesCompanion loanChange) { return into( loanChanges, ).insertReturning(loanChange, mode: InsertMode.insertOrReplace); } Stream> watchLoanChanges(Loan loan) { return (select(loanChanges) ..where((c) => c.loanId.equals(loan.id))).watch(); } Future deleteLoanChange(int id) { return (delete(loanChanges)..where((c) => c.id.equals(id))).go(); } } @DriftAccessor(tables: [TransactionTemplates, ExpenseCategories, Beneficiaries]) class TransactionTemplatesDao extends DatabaseAccessor with _$TransactionTemplatesDaoMixin { TransactionTemplatesDao(OkaneDatabase db) : super(db); Stream> transactionTemplatesStream( Account account, ) { return (select(transactionTemplates) ..where((b) => b.accountId.equals(account.id))) .join([ leftOuterJoin( beneficiaries, beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId), ), leftOuterJoin( expenseCategories, expenseCategories.id.equalsExp( transactionTemplates.expenseCategoryId, ), ), ]) .watch() .map((rows) { return rows.map((row) { return ( template: row.readTable(transactionTemplates), beneficiary: row.readTable(beneficiaries), expenseCategory: row.readTable(expenseCategories), ); }).toList(); }); } Future> getTransactionTemplates( Account? account, ) { if (account == null) { return Future.value(List.empty()); } return (select(transactionTemplates) ..where((b) => b.accountId.equals(account.id))) .join([ leftOuterJoin( beneficiaries, beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId), ), leftOuterJoin( expenseCategories, expenseCategories.id.equalsExp( transactionTemplates.expenseCategoryId, ), ), ]) .get() .then((rows) { return rows.map((row) { return ( template: row.readTable(transactionTemplates), beneficiary: row.readTable(beneficiaries), expenseCategory: row.readTable(expenseCategories), ); }).toList(); }); } Future upsertTemplate( TransactionTemplatesCompanion template, ) { return into( transactionTemplates, ).insertReturning(template, mode: InsertMode.insertOrReplace); } Future deleteTemplate(TransactionTemplate template) { return (delete(transactionTemplates) ..where((t) => t.id.equals(template.id))).go(); } } @DriftAccessor(tables: [TransactionTemplates, RecurringTransactions]) class RecurringTransactionsDao extends DatabaseAccessor with _$RecurringTransactionsDaoMixin { RecurringTransactionsDao(OkaneDatabase db) : super(db); Stream> recurringTransactionsStream( Account account, ) { return (select(recurringTransactions) ..where((b) => b.accountId.equals(account.id))) .join([ leftOuterJoin( transactionTemplates, transactionTemplates.id.equalsExp(recurringTransactions.templateId), ), leftOuterJoin( beneficiaries, beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId), ), ]) .watch() .map((rows) { return rows.map((row) { return ( recurring: row.readTable(recurringTransactions), beneficiary: row.readTable(beneficiaries), template: row.readTable(transactionTemplates), ); }).toList(); }); } Future> getRecurringTransactions( Account? account, ) { if (account == null) { return Future.value(List.empty()); } return (select(recurringTransactions) ..where((b) => b.accountId.equals(account.id))) .join([ leftOuterJoin( transactionTemplates, transactionTemplates.id.equalsExp(recurringTransactions.templateId), ), leftOuterJoin( beneficiaries, beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId), ), ]) .get() .then((rows) { return rows.map((row) { return ( recurring: row.readTable(recurringTransactions), beneficiary: row.readTable(beneficiaries), template: row.readTable(transactionTemplates), ); }).toList(); }); } Future upsertRecurringTransaction(RecurringTransactionsCompanion r) { return into(recurringTransactions).insertOnConflictUpdate(r); } Future deleteTemplate(RecurringTransactionDto dto) async { await db.transactionTemplatesDao.deleteTemplate(dto.template); await (delete(recurringTransactions) ..where((t) => t.id.equals(dto.recurring.id))).go(); } }