Added transaction generation to sample data generation in the API.
This commit is contained in:
parent
f5c75ccf35
commit
3fa2938f48
|
@ -141,9 +141,9 @@ CREATE TABLE account_value_record (
|
|||
timestamp TEXT NOT NULL,
|
||||
account_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'BALANCE',
|
||||
balance INTEGER NOT NULL,
|
||||
value INTEGER NOT NULL,
|
||||
currency TEXT NOT NULL,
|
||||
CONSTRAINT fk_balance_record_account
|
||||
CONSTRAINT fk_account_value_record_account
|
||||
FOREIGN KEY (account_id) REFERENCES account(id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
|
|
@ -6,6 +6,8 @@ import account.model;
|
|||
import util.money;
|
||||
import history.model;
|
||||
|
||||
import std.datetime : SysTime;
|
||||
|
||||
interface AccountRepository {
|
||||
Optional!Account findById(ulong id);
|
||||
Account insert(AccountType type, string numberSuffix, string name, Currency currency, string description);
|
||||
|
@ -17,3 +19,16 @@ interface AccountRepository {
|
|||
void setCreditCardProperties(ulong id, in AccountCreditCardProperties props);
|
||||
History getHistory(ulong id);
|
||||
}
|
||||
|
||||
interface AccountJournalEntryRepository {
|
||||
Optional!AccountJournalEntry findById(ulong id);
|
||||
AccountJournalEntry insert(
|
||||
SysTime timestamp,
|
||||
ulong accountId,
|
||||
ulong transactionId,
|
||||
ulong amount,
|
||||
AccountJournalEntryType type,
|
||||
Currency currency
|
||||
);
|
||||
void deleteById(ulong id);
|
||||
}
|
||||
|
|
|
@ -185,3 +185,63 @@ SQL",
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteAccountJournalEntryRepository : AccountJournalEntryRepository {
|
||||
private Database db;
|
||||
this(Database db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
Optional!AccountJournalEntry findById(ulong id) {
|
||||
return util.sqlite.findById(db, "account_journal_entry", &parseEntry, id);
|
||||
}
|
||||
|
||||
AccountJournalEntry insert(
|
||||
SysTime timestamp,
|
||||
ulong accountId,
|
||||
ulong transactionId,
|
||||
ulong amount,
|
||||
AccountJournalEntryType type,
|
||||
Currency currency
|
||||
) {
|
||||
util.sqlite.update(
|
||||
db,
|
||||
"INSERT INTO account_journal_entry
|
||||
(timestamp, account_id, transaction_id, amount, type, currency)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
timestamp.toISOExtString(),
|
||||
accountId,
|
||||
transactionId,
|
||||
amount,
|
||||
type,
|
||||
currency.code
|
||||
);
|
||||
ulong id = db.lastInsertRowid();
|
||||
return findById(id).orElseThrow();
|
||||
}
|
||||
|
||||
void deleteById(ulong id) {
|
||||
util.sqlite.deleteById(db, "account_journal_entry", id);
|
||||
}
|
||||
|
||||
static AccountJournalEntry parseEntry(Row row) {
|
||||
string typeStr = row.peek!(string, PeekMode.slice)(5);
|
||||
AccountJournalEntryType type;
|
||||
if (typeStr == "CREDIT") {
|
||||
type = AccountJournalEntryType.CREDIT;
|
||||
} else if (typeStr == "DEBIT") {
|
||||
type = AccountJournalEntryType.DEBIT;
|
||||
} else {
|
||||
throw new Exception("Invalid account journal entry type: " ~ typeStr);
|
||||
}
|
||||
return AccountJournalEntry(
|
||||
row.peek!ulong(0),
|
||||
SysTime.fromISOExtString(row.peek!string(1)),
|
||||
row.peek!ulong(2),
|
||||
row.peek!ulong(3),
|
||||
row.peek!ulong(4),
|
||||
type,
|
||||
Currency.ofCode(row.peek!(string, PeekMode.slice)(6))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ import std.traits : EnumMembers;
|
|||
import util.money;
|
||||
|
||||
struct AccountType {
|
||||
const string id;
|
||||
const string name;
|
||||
const bool debitsPositive;
|
||||
immutable string id;
|
||||
immutable string name;
|
||||
immutable bool debitsPositive;
|
||||
|
||||
static AccountType fromId(string id) {
|
||||
static foreach (t; ALL_ACCOUNT_TYPES) {
|
||||
|
@ -28,17 +28,45 @@ enum AccountTypes : AccountType {
|
|||
immutable(AccountType[]) ALL_ACCOUNT_TYPES = cast(AccountType[]) [ EnumMembers!AccountTypes ];
|
||||
|
||||
struct Account {
|
||||
const ulong id;
|
||||
const SysTime createdAt;
|
||||
const bool archived;
|
||||
const AccountType type;
|
||||
const string numberSuffix;
|
||||
const string name;
|
||||
const Currency currency;
|
||||
const string description;
|
||||
immutable ulong id;
|
||||
immutable SysTime createdAt;
|
||||
immutable bool archived;
|
||||
immutable AccountType type;
|
||||
immutable string numberSuffix;
|
||||
immutable string name;
|
||||
immutable Currency currency;
|
||||
immutable string description;
|
||||
}
|
||||
|
||||
struct AccountCreditCardProperties {
|
||||
const ulong account_id;
|
||||
const long creditLimit;
|
||||
immutable ulong account_id;
|
||||
immutable long creditLimit;
|
||||
}
|
||||
|
||||
enum AccountJournalEntryType : string {
|
||||
CREDIT = "CREDIT",
|
||||
DEBIT = "DEBIT"
|
||||
}
|
||||
|
||||
struct AccountJournalEntry {
|
||||
immutable ulong id;
|
||||
immutable SysTime timestamp;
|
||||
immutable ulong accountId;
|
||||
immutable ulong transactionId;
|
||||
immutable ulong amount;
|
||||
immutable AccountJournalEntryType type;
|
||||
immutable Currency currency;
|
||||
}
|
||||
|
||||
enum AccountValueRecordType : string {
|
||||
BALANCE = "BALANCE"
|
||||
}
|
||||
|
||||
struct AccountValueRecord {
|
||||
immutable ulong id;
|
||||
immutable SysTime timestamp;
|
||||
immutable ulong accountId;
|
||||
immutable AccountValueRecordType type;
|
||||
immutable long value;
|
||||
immutable Currency currency;
|
||||
}
|
||||
|
|
|
@ -58,11 +58,11 @@ SQL",
|
|||
static Attachment parseAttachment(Row row) {
|
||||
return Attachment(
|
||||
row.peek!ulong(0),
|
||||
SysTime.fromISOExtString(row.peek!string(1), UTC()),
|
||||
parseISOTimestamp(row, 1),
|
||||
row.peek!string(2),
|
||||
row.peek!string(3),
|
||||
row.peek!ulong(4),
|
||||
row.peek!(ubyte[])(5)
|
||||
parseBlob(row, 5)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ module attachment.model;
|
|||
import std.datetime;
|
||||
|
||||
struct Attachment {
|
||||
ulong id;
|
||||
SysTime uploadedAt;
|
||||
string filename;
|
||||
string contentType;
|
||||
ulong size;
|
||||
ubyte[] content;
|
||||
immutable ulong id;
|
||||
immutable SysTime uploadedAt;
|
||||
immutable string filename;
|
||||
immutable string contentType;
|
||||
immutable ulong size;
|
||||
immutable ubyte[] content;
|
||||
}
|
||||
|
|
|
@ -25,8 +25,18 @@ interface PropertiesRepository {
|
|||
* gateway for all data access operations for a profile.
|
||||
*/
|
||||
interface ProfileDataSource {
|
||||
import account.data : AccountRepository;
|
||||
import account.data;
|
||||
import transaction.data;
|
||||
|
||||
PropertiesRepository getPropertiesRepository();
|
||||
|
||||
AccountRepository getAccountRepository();
|
||||
AccountJournalEntryRepository getAccountJournalEntryRepository();
|
||||
|
||||
TransactionVendorRepository getTransactionVendorRepository();
|
||||
TransactionCategoryRepository getTransactionCategoryRepository();
|
||||
TransactionTagRepository getTransactionTagRepository();
|
||||
TransactionRepository getTransactionRepository();
|
||||
|
||||
void doTransaction(void delegate () dg);
|
||||
}
|
||||
|
|
|
@ -141,6 +141,8 @@ class SqlitePropertiesRepository : PropertiesRepository {
|
|||
class SqliteProfileDataSource : ProfileDataSource {
|
||||
import account.data;
|
||||
import account.data_impl_sqlite;
|
||||
import transaction.data;
|
||||
import transaction.data_impl_sqlite;
|
||||
|
||||
const SCHEMA = import("schema.sql");
|
||||
private const string dbPath;
|
||||
|
@ -164,4 +166,28 @@ class SqliteProfileDataSource : ProfileDataSource {
|
|||
AccountRepository getAccountRepository() {
|
||||
return new SqliteAccountRepository(db);
|
||||
}
|
||||
|
||||
AccountJournalEntryRepository getAccountJournalEntryRepository() {
|
||||
return new SqliteAccountJournalEntryRepository(db);
|
||||
}
|
||||
|
||||
TransactionVendorRepository getTransactionVendorRepository() {
|
||||
return new SqliteTransactionVendorRepository(db);
|
||||
}
|
||||
|
||||
TransactionCategoryRepository getTransactionCategoryRepository() {
|
||||
return new SqliteTransactionCategoryRepository(db);
|
||||
}
|
||||
|
||||
TransactionTagRepository getTransactionTagRepository() {
|
||||
return new SqliteTransactionTagRepository(db);
|
||||
}
|
||||
|
||||
TransactionRepository getTransactionRepository() {
|
||||
return new SqliteTransactionRepository(db);
|
||||
}
|
||||
|
||||
void doTransaction(void delegate () dg) {
|
||||
util.sqlite.doTransaction(db, dg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -179,3 +179,59 @@ class SqliteTransactionTagRepository : TransactionTagRepository {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteTransactionRepository : TransactionRepository {
|
||||
private const TABLE_NAME = "\"transaction\"";
|
||||
private Database db;
|
||||
this(Database db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
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,
|
||||
vendorId.asNullable,
|
||||
categoryId.asNullable
|
||||
);
|
||||
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),
|
||||
Optional!(ulong).of(row.peek!(Nullable!ulong)(6)),
|
||||
Optional!(ulong).of(row.peek!(Nullable!ulong)(7))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,43 +6,43 @@ import std.datetime;
|
|||
import util.money;
|
||||
|
||||
struct TransactionVendor {
|
||||
const ulong id;
|
||||
const string name;
|
||||
const string description;
|
||||
immutable ulong id;
|
||||
immutable string name;
|
||||
immutable string description;
|
||||
}
|
||||
|
||||
struct TransactionCategory {
|
||||
const ulong id;
|
||||
const Optional!ulong parentId;
|
||||
const string name;
|
||||
const string description;
|
||||
const string color;
|
||||
immutable ulong id;
|
||||
immutable Optional!ulong parentId;
|
||||
immutable string name;
|
||||
immutable string description;
|
||||
immutable string color;
|
||||
}
|
||||
|
||||
struct TransactionTag {
|
||||
const ulong id;
|
||||
const string name;
|
||||
immutable ulong id;
|
||||
immutable string name;
|
||||
}
|
||||
|
||||
struct Transaction {
|
||||
const ulong id;
|
||||
immutable ulong id;
|
||||
/// The time at which the transaction happened.
|
||||
const SysTime timestamp;
|
||||
immutable SysTime timestamp;
|
||||
/// The time at which the transaction entity was saved.
|
||||
const SysTime addedAt;
|
||||
const ulong amount;
|
||||
const Currency currency;
|
||||
const string description;
|
||||
const Optional!ulong vendorId;
|
||||
const Optional!ulong categoryId;
|
||||
immutable SysTime addedAt;
|
||||
immutable ulong amount;
|
||||
immutable Currency currency;
|
||||
immutable string description;
|
||||
immutable Optional!ulong vendorId;
|
||||
immutable Optional!ulong categoryId;
|
||||
}
|
||||
|
||||
struct TransactionLineItem {
|
||||
const ulong id;
|
||||
const ulong transactionId;
|
||||
const long valuePerItem;
|
||||
const ulong quantity;
|
||||
const uint idx;
|
||||
const string description;
|
||||
const Optional!ulong categoryId;
|
||||
immutable ulong id;
|
||||
immutable ulong transactionId;
|
||||
immutable long valuePerItem;
|
||||
immutable ulong quantity;
|
||||
immutable uint idx;
|
||||
immutable string description;
|
||||
immutable Optional!ulong categoryId;
|
||||
}
|
||||
|
|
|
@ -2,3 +2,4 @@ module transaction;
|
|||
|
||||
public import transaction.data;
|
||||
public import transaction.model;
|
||||
public import transaction.service;
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
module transaction.service;
|
||||
|
||||
import handy_httpd.components.optional;
|
||||
import std.datetime;
|
||||
|
||||
import transaction.model;
|
||||
import transaction.data;
|
||||
import profile.data;
|
||||
import util.money;
|
||||
import account.model;
|
||||
|
||||
void addTransaction(
|
||||
ProfileDataSource ds,
|
||||
SysTime timestamp,
|
||||
SysTime addedAt,
|
||||
ulong amount,
|
||||
Currency currency,
|
||||
string description,
|
||||
Optional!ulong vendorId,
|
||||
Optional!ulong categoryId,
|
||||
Optional!ulong creditedAccountId,
|
||||
Optional!ulong debitedAccountId,
|
||||
TransactionLineItem[] lineItems
|
||||
// TODO: Add attachments and tags!
|
||||
) {
|
||||
if (creditedAccountId.isNull && debitedAccountId.isNull) {
|
||||
throw new Exception("At least one account must be linked to a transaction.");
|
||||
}
|
||||
ds.doTransaction(() {
|
||||
auto journalEntryRepo = ds.getAccountJournalEntryRepository();
|
||||
auto txRepo = ds.getTransactionRepository();
|
||||
Transaction tx = txRepo.insert(
|
||||
timestamp,
|
||||
addedAt,
|
||||
amount,
|
||||
currency,
|
||||
description,
|
||||
vendorId,
|
||||
categoryId
|
||||
);
|
||||
if (creditedAccountId) {
|
||||
journalEntryRepo.insert(
|
||||
timestamp,
|
||||
creditedAccountId.value,
|
||||
tx.id,
|
||||
amount,
|
||||
AccountJournalEntryType.CREDIT,
|
||||
currency
|
||||
);
|
||||
}
|
||||
if (debitedAccountId) {
|
||||
journalEntryRepo.insert(
|
||||
timestamp,
|
||||
debitedAccountId.value,
|
||||
tx.id,
|
||||
amount,
|
||||
AccountJournalEntryType.DEBIT,
|
||||
currency
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -8,11 +8,11 @@ import std.traits : isSomeString, EnumMembers;
|
|||
*/
|
||||
struct Currency {
|
||||
/// The common 3-character code for the currency, like "USD".
|
||||
const char[3] code;
|
||||
immutable char[3] code;
|
||||
/// The number of digits after the decimal place that the currency supports.
|
||||
const ubyte fractionalDigits;
|
||||
immutable ubyte fractionalDigits;
|
||||
/// The ISO 4217 numeric code for the currency.
|
||||
const ushort numericCode;
|
||||
immutable ushort numericCode;
|
||||
|
||||
static Currency ofCode(S)(S code) if (isSomeString!S) {
|
||||
if (code.length != 3) {
|
||||
|
@ -50,8 +50,8 @@ unittest {
|
|||
* so for example, with USD currency, a value of 123 indicates $1.23.
|
||||
*/
|
||||
struct MoneyValue {
|
||||
const Currency currency;
|
||||
const long value;
|
||||
immutable Currency currency;
|
||||
immutable long value;
|
||||
|
||||
int opCmp(in MoneyValue other) const {
|
||||
if (other.currency != this.currency) return 0;
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
module util.sample_data;
|
||||
|
||||
import slf4d;
|
||||
import handy_httpd.components.optional;
|
||||
|
||||
import auth;
|
||||
import profile;
|
||||
import account;
|
||||
import transaction;
|
||||
import util.money;
|
||||
|
||||
import std.random;
|
||||
import std.conv;
|
||||
import std.array;
|
||||
import std.datetime;
|
||||
|
||||
void generateSampleData() {
|
||||
UserRepository userRepo = new FileSystemUserRepository;
|
||||
|
@ -20,7 +23,13 @@ void generateSampleData() {
|
|||
|
||||
const int userCount = uniform(5, 10);
|
||||
for (int i = 0; i < userCount; i++) {
|
||||
try {
|
||||
generateRandomUser(i, userRepo);
|
||||
} catch (Throwable t) {
|
||||
import std.stdio;
|
||||
stderr.writeln(t);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
info("Random sample data generation complete.");
|
||||
}
|
||||
|
@ -48,6 +57,29 @@ void generateRandomProfile(int idx, ProfileRepository profileRepo) {
|
|||
for (int i = 0; i < accountCount; i++) {
|
||||
generateRandomAccount(i, ds);
|
||||
}
|
||||
|
||||
auto vendorRepo = ds.getTransactionVendorRepository();
|
||||
const int vendorCount = uniform(5, 30);
|
||||
for (int i = 0; i < vendorCount; i++) {
|
||||
vendorRepo.insert("Test Vendor " ~ to!string(i), "Testing vendor for sample data.");
|
||||
}
|
||||
infoF!" Generated %d random vendors."(vendorCount);
|
||||
|
||||
auto tagRepo = ds.getTransactionTagRepository();
|
||||
const int tagCount = uniform(5, 30);
|
||||
for (int i = 0; i < tagCount; i++) {
|
||||
tagRepo.insert("test-tag-" ~ to!string(i));
|
||||
}
|
||||
infoF!" Generated %d random tags."(tagCount);
|
||||
|
||||
auto categoryRepo = ds.getTransactionCategoryRepository();
|
||||
const int categoryCount = uniform(5, 30);
|
||||
for (int i = 0; i < categoryCount; i++) {
|
||||
categoryRepo.insert(Optional!ulong.empty, "Test Category " ~ to!string(i), "Testing category.", "FFFFFF");
|
||||
}
|
||||
infoF!" Generated %d random categories."(categoryCount);
|
||||
|
||||
generateRandomTransactions(ds);
|
||||
}
|
||||
|
||||
void generateRandomAccount(int idx, ProfileDataSource ds) {
|
||||
|
@ -58,7 +90,6 @@ void generateRandomAccount(int idx, ProfileDataSource ds) {
|
|||
AccountType type = choice(ALL_ACCOUNT_TYPES);
|
||||
Currency currency = choice(ALL_CURRENCIES);
|
||||
string description = "This is a testing account generated by util.sample_data.generateRandomAccount().";
|
||||
infoF!" Generating random account: %s, ...%s"(name, numberSuffix);
|
||||
Account account = accountRepo.insert(
|
||||
type,
|
||||
numberSuffix,
|
||||
|
@ -66,4 +97,65 @@ void generateRandomAccount(int idx, ProfileDataSource ds) {
|
|||
currency,
|
||||
description
|
||||
);
|
||||
infoF!" Generated random account: %s, #%s"(name, numberSuffix);
|
||||
}
|
||||
|
||||
void generateRandomTransactions(ProfileDataSource ds) {
|
||||
const bool hasVendor = uniform01() > 0.3;
|
||||
const bool hasCategory = uniform01() > 0.2;
|
||||
const TransactionVendor[] vendors = ds.getTransactionVendorRepository.findAll();
|
||||
const TransactionCategory[] categories = ds.getTransactionCategoryRepository()
|
||||
.findAllByParentId(Optional!ulong.empty);
|
||||
const TransactionTag[] tags = ds.getTransactionTagRepository().findAll();
|
||||
const Account[] accounts = ds.getAccountRepository().findAll();
|
||||
|
||||
SysTime now = Clock.currTime(UTC());
|
||||
SysTime timestamp = Clock.currTime(UTC()) - seconds(1);
|
||||
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
Optional!ulong vendorId;
|
||||
if (hasVendor) {
|
||||
vendorId = Optional!ulong.of(choice(vendors).id);
|
||||
}
|
||||
Optional!ulong categoryId;
|
||||
if (hasCategory) {
|
||||
categoryId = Optional!ulong.of(choice(categories).id);
|
||||
}
|
||||
Optional!ulong creditedAccountId;
|
||||
Optional!ulong debitedAccountId;
|
||||
Account primaryAccount = choice(accounts);
|
||||
Optional!ulong secondaryAccount;
|
||||
if (uniform01() < 0.25) {
|
||||
foreach (acc; accounts) {
|
||||
if (acc.id != primaryAccount.id && acc.currency == primaryAccount.currency) {
|
||||
secondaryAccount.value = acc.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (uniform01() > 0.5) {
|
||||
creditedAccountId = Optional!ulong.of(primaryAccount.id);
|
||||
if (secondaryAccount) debitedAccountId = secondaryAccount;
|
||||
} else {
|
||||
debitedAccountId = Optional!ulong.of(primaryAccount.id);
|
||||
if (secondaryAccount) creditedAccountId = secondaryAccount;
|
||||
}
|
||||
ulong value = uniform(0, 1_000_000);
|
||||
|
||||
addTransaction(
|
||||
ds,
|
||||
timestamp,
|
||||
now,
|
||||
value,
|
||||
primaryAccount.currency,
|
||||
"Test transaction " ~ to!string(i),
|
||||
vendorId,
|
||||
categoryId,
|
||||
creditedAccountId,
|
||||
debitedAccountId,
|
||||
[]
|
||||
);
|
||||
infoF!" Generated transaction %d"(i);
|
||||
timestamp -= seconds(uniform(10, 1_000_000));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
module util.sqlite;
|
||||
|
||||
import std.datetime;
|
||||
|
||||
import slf4d;
|
||||
import handy_httpd.components.optional;
|
||||
import d2sqlite3;
|
||||
|
@ -31,7 +33,7 @@ Optional!T findOne(T, Args...)(Database db, string query, T function(Row) result
|
|||
* Returns: An optional result.
|
||||
*/
|
||||
Optional!T findById(T)(Database db, string table, T function(Row) resultMapper, ulong id) {
|
||||
Statement stmt = db.prepare("SELECT * FROM " ~ table);
|
||||
Statement stmt = db.prepare("SELECT * FROM " ~ table ~ " WHERE id = ?");
|
||||
stmt.bind(1, id);
|
||||
ResultRange result = stmt.execute();
|
||||
if (result.empty) return Optional!T.empty;
|
||||
|
@ -123,3 +125,44 @@ T doTransaction(T)(Database db, T delegate() dg) {
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a "SELECT COUNT..." query on a database.
|
||||
* Params:
|
||||
* db = The database to use.
|
||||
* query = The query to use, which must return an integer as its sole result.
|
||||
* args = Arguments to provide to the query.
|
||||
* Returns: The count returned by the query.
|
||||
*/
|
||||
ulong count(Args...)(Database db, string query, Args args) {
|
||||
Statement stmt = db.prepare(query);
|
||||
stmt.bindAll(args);
|
||||
ResultRange result = stmt.execute();
|
||||
if (result.empty) return 0;
|
||||
return result.front.peek!ulong(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an ISO-8601 UTC timestamp from a result row.
|
||||
* Params:
|
||||
* row = The row to read from.
|
||||
* idx = The column index in the row to read.
|
||||
* Returns: The timestamp that was read.
|
||||
*/
|
||||
SysTime parseISOTimestamp(Row row, size_t idx) {
|
||||
return SysTime.fromISOExtString(
|
||||
row.peek!(string, PeekMode.slice)(idx),
|
||||
UTC()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a set of bytes from a result row.
|
||||
* Params:
|
||||
* row = The row to read from.
|
||||
* idx = The column index in the row to read.
|
||||
* Returns: The blob data that was read.
|
||||
*/
|
||||
immutable(ubyte[]) parseBlob(Row row, size_t idx) {
|
||||
return row.peek!(ubyte[], PeekMode.slice)(idx).idup;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue