module transaction.data_impl_sqlite; import handy_http_primitives : Optional; import std.datetime; import std.typecons; import d2sqlite3; import transaction.model; import transaction.data; import transaction.api; import util.sqlite; import util.money; import util.pagination; import util.data; class SqliteTransactionVendorRepository : TransactionVendorRepository { private Database db; this(Database db) { this.db = db; } Optional!TransactionVendor findById(ulong id) { return util.sqlite.findById(db, "transaction_vendor", &parseVendor, id); } TransactionVendor[] findAll() { return util.sqlite.findAll( db, "SELECT * FROM transaction_vendor ORDER BY name ASC", &parseVendor ); } bool existsByName(string name) { return util.sqlite.exists(db, "SELECT id FROM transaction_vendor WHERE name = ?", name); } TransactionVendor insert(string name, string description) { util.sqlite.update( db, "INSERT INTO transaction_vendor (name, description) VALUES (?, ?)", name, description ); ulong id = db.lastInsertRowid(); return findById(id).orElseThrow(); } void deleteById(ulong id) { util.sqlite.deleteById(db, "transaction_vendor", id); } TransactionVendor updateById(ulong id, string name, string description) { util.sqlite.update( db, "UPDATE transaction_vendor SET name = ?, description = ? WHERE id = ?", name, description, id ); return findById(id).orElseThrow(); } private static TransactionVendor parseVendor(Row row) { return TransactionVendor( row.peek!ulong(0), row.peek!string(1), row.peek!string(2) ); } } class SqliteTransactionCategoryRepository : TransactionCategoryRepository { private Database db; this(Database db) { this.db = db; } Optional!TransactionCategory findById(ulong id) { return util.sqlite.findById(db, "transaction_category", &parseCategory, id); } TransactionCategory[] findAllByParentId(Optional!ulong parentId) { if (parentId) { return util.sqlite.findAll( db, "SELECT * FROM transaction_category WHERE parent_id = ? ORDER BY name ASC", &parseCategory, parentId.value ); } return util.sqlite.findAll( db, "SELECT * FROM transaction_category WHERE parent_id IS NULL ORDER BY name ASC", &parseCategory ); } TransactionCategory insert(Optional!ulong parentId, string name, string description, string color) { util.sqlite.update( db, "INSERT INTO transaction_category (parent_id, name, description, color) VALUES (?, ?, ?, ?)", toNullable(parentId), name, description, color ); ulong id = db.lastInsertRowid(); return findById(id).orElseThrow(); } void deleteById(ulong id) { util.sqlite.deleteById(db, "transaction_category", id); } TransactionCategory updateById(ulong id, string name, string description, string color) { util.sqlite.update( db, "UPDATE transaction_category SET name = ?, description = ?, color = ? WHERE id = ?", name, description, color, id ); return findById(id).orElseThrow(); } private static TransactionCategory parseCategory(Row row) { import std.typecons; return TransactionCategory( row.peek!ulong(0), toOptional(row.peek!(Nullable!ulong)(1)), row.peek!string(2), row.peek!string(3), row.peek!string(4) ); } } class SqliteTransactionTagRepository : TransactionTagRepository { private Database db; this(Database db) { this.db = db; } string[] findAllByTransactionId(ulong transactionId) { return util.sqlite.findAll( db, "SELECT tag FROM transaction_tag WHERE transaction_id = ? ORDER BY tag", r => r.peek!string(0), transactionId ); } void updateTags(ulong transactionId, string[] tags) { util.sqlite.update( db, "DELETE FROM transaction_tag WHERE transaction_id = ?", transactionId ); foreach (tag; tags) { util.sqlite.update( db, "INSERT INTO transaction_tag (transaction_id, tag) VALUES (?, ?)", transactionId, tag ); } } string[] findAll() { return util.sqlite.findAll( db, "SELECT DISTINCT tag FROM transaction_tag ORDER BY tag", r => r.peek!string(0) ); } } class SqliteTransactionRepository : TransactionRepository { private const TABLE_NAME = "\"transaction\""; private Database db; this(Database db) { this.db = db; } Page!TransactionsListItem findAll(PageRequest pr) { const BASE_QUERY = import("sql/get_transactions.sql"); // TODO: Implement filtering or something! import std.array; const string countQuery = "SELECT COUNT(ID) FROM " ~ TABLE_NAME; auto sqlBuilder = appender!string; sqlBuilder ~= BASE_QUERY; sqlBuilder ~= " "; sqlBuilder ~= pr.toSql(); string query = sqlBuilder[]; TransactionsListItem[] results = util.sqlite.findAll(db, query, (row) { TransactionsListItem item; item.id = row.peek!ulong(0); item.timestamp = row.peek!string(1); item.addedAt = row.peek!string(2); item.amount = row.peek!ulong(3); item.currency = Currency.ofCode(row.peek!(string, PeekMode.slice)(4)); item.description = row.peek!string(5); Nullable!ulong vendorId = row.peek!(Nullable!ulong)(6); if (!vendorId.isNull) { string vendorName = row.peek!string(7); item.vendor = Optional!TransactionsListItemVendor.of( TransactionsListItemVendor(vendorId.get, vendorName)); } Nullable!ulong categoryId = row.peek!(Nullable!ulong)(8); if (!categoryId.isNull) { string categoryName = row.peek!string(9); string categoryColor = row.peek!string(10); item.category = Optional!TransactionsListItemCategory.of( TransactionsListItemCategory(categoryId.get, categoryName, categoryColor)); } Nullable!ulong creditedAccountId = row.peek!(Nullable!ulong)(11); if (!creditedAccountId.isNull) { ulong id = creditedAccountId.get; string name = row.peek!string(12); string type = row.peek!string(13); string suffix = row.peek!string(14); item.creditedAccount = Optional!TransactionsListItemAccount.of( TransactionsListItemAccount(id, name, type, suffix)); } Nullable!ulong debitedAccountId = row.peek!(Nullable!ulong)(15); if (!debitedAccountId.isNull) { ulong id = debitedAccountId.get; string name = row.peek!string(16); string type = row.peek!string(17); string suffix = row.peek!string(18); item.debitedAccount = Optional!TransactionsListItemAccount.of( TransactionsListItemAccount(id, name, type, suffix)); } string tagsStr = row.peek!string(19); if (tagsStr.length > 0) { import std.string : split; item.tags = tagsStr.split(","); } return item; }); ulong totalCount = util.sqlite.count(db, countQuery); return Page!(TransactionsListItem).of(results, pr, totalCount); } Optional!Transaction findById(ulong id) { return util.sqlite.findById(db, TABLE_NAME, &parseTransaction, id); } Transaction insert( SysTime timestamp, SysTime addedAt, ulong amount, Currency currency, string description, Optional!ulong vendorId, Optional!ulong categoryId ) { util.sqlite.update( db, "INSERT INTO " ~ TABLE_NAME ~ " (timestamp, added_at, amount, currency, description, vendor_id, category_id) VALUES (?, ?, ?, ?, ?, ?, ?)", timestamp.toISOExtString(), addedAt.toISOExtString(), amount, currency.code, description, toNullable(vendorId), toNullable(categoryId) ); ulong id = db.lastInsertRowid(); return findById(id).orElseThrow(); } void deleteById(ulong id) { util.sqlite.deleteById(db, TABLE_NAME, id); } static Transaction parseTransaction(Row row) { import std.typecons : Nullable; return Transaction( row.peek!ulong(0), SysTime.fromISOExtString(row.peek!string(1)), SysTime.fromISOExtString(row.peek!string(2)), row.peek!ulong(3), Currency.ofCode(row.peek!(string, PeekMode.slice)(4)), row.peek!string(5), toOptional(row.peek!(Nullable!ulong)(6)), toOptional(row.peek!(Nullable!ulong)(7)) ); } }