add database migration and UserRepository wrapper

- implemented DB migration

- added UserRepository for DB interactions

- inserted initial test record
This commit is contained in:
2026-04-09 08:00:44 +03:00
parent 062b64bce8
commit fe9fb77a02
18 changed files with 294 additions and 24 deletions
+2
View File
@@ -1,3 +1,5 @@
build/
# ---> C++
# Prerequisites
*.d
+3
View File
@@ -22,6 +22,9 @@ add_executable(auth_service
src/http/controllers/health_controller.cpp
src/db/sqllite_db.cpp
src/db/statement.cpp
src/db/transaction.cpp
src/db/migrations_runner.cpp
src/repo/user_repository.cpp
)
target_include_directories(auth_service
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+30
View File
@@ -0,0 +1,30 @@
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email
ON users(email);
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token_hash TEXT NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
revoked_at TEXT,
FOREIGN KEY(user_id) REFERENCES users(id)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token_hash
ON sessions(token_hash);
CREATE INDEX IF NOT EXISTS idx_sessions_user_id
ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at
ON sessions(expires_at);
+14 -22
View File
@@ -1,7 +1,9 @@
#include "application/application.hpp"
// #include <nlohmann/json.hpp>
#include "db/migrations_runner.hpp"
#include "db/sqllite_db.hpp"
#include "http/controllers/health_controller.hpp"
#include "repo/user_repository.hpp"
#include <drogon/drogon.h>
#include <format>
@@ -27,30 +29,20 @@ int Application::run() const {
SqlliteDb db(settings_.db_path);
db.initialize();
// {
// db.execute("CREATE TABLE IF NOT EXISTS healthcheck_tmp ("
// "id INTEGER PRIMARY KEY AUTOINCREMENT, "
// "name TEXT NOT NULL"
// ");");
MigrationsRunner runner(db);
runner.run_file("migrations/001_init.sql");
// {
// auto insert_stmt = db.prepare("INSERT INTO healthcheck_tmp(name) VALUES(?1);");
// insert_stmt.bind_text(1, "boot");
// insert_stmt.execute();
// }
UserRepository user_repository(db);
const auto existing_user = user_repository.find_by_email("test@example.com");
if(!existing_user.has_value()) {
const auto created_user = user_repository.create(
"test@example.com", "dummy_hash", "2026-04-05T12:00:00Z", "2026-04-05T12:00:00Z");
// {
// auto select_stmt =
// db.prepare("SELECT id, name FROM healthcheck_tmp ORDER BY id DESC LIMIT 1;");
// if(select_stmt.step()) {
// const auto id = select_stmt.column_int64(0);
// const auto name = select_stmt.column_text(1);
// std::cout << "id=" << id << ", value=" << name << '\n';
// }
// }
// }
std::cout << "Created user: id=" << created_user.id << ", email=" << created_user.email << '\n';
} else {
std::cout << "User already exists: id=" << existing_user->id
<< ", email=" << existing_user->email << '\n';
}
register_routes();
drogon::app().addListener(settings_.host, settings_.port);
+22
View File
@@ -0,0 +1,22 @@
#include "db/migrations_runner.hpp"
#include "db/sqllite_db.hpp"
#include <fstream>
#include <sstream>
#include <stdexcept>
MigrationsRunner::MigrationsRunner(SqlliteDb &db)
: db_(db) {}
void MigrationsRunner::run_file(const std::string &path) {
std::ifstream file(path);
if(!file) {
throw std::runtime_error("Failed open maigration file: " + path);
}
std::stringstream buff;
buff << file.rdbuf();
db_.execute(buff.str());
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <string>
class SqlliteDb;
class MigrationsRunner {
public:
explicit MigrationsRunner(SqlliteDb &db);
void run_file(const std::string &path);
private:
SqlliteDb &db_;
};
+8
View File
@@ -70,3 +70,11 @@ void SqlliteDb::closeDb() {
db_ = nullptr;
}
}
Transaction SqlliteDb::begin_transaction(){
return Transaction{*this};
}
std::int64_t SqlliteDb::last_insert_rowid() const noexcept{
return sqlite3_last_insert_rowid(db_);
}
+3
View File
@@ -1,6 +1,7 @@
#pragma once
#include "db/statement.hpp"
#include "db/transaction.hpp"
#include <sqlite3.h>
#include <string>
@@ -21,8 +22,10 @@ public:
void initialize();
Statement prepare(std::string_view sql) const;
Transaction begin_transaction();
sqlite3 *native_handle() const noexcept;
std::int64_t last_insert_rowid() const noexcept;
private:
void closeDb();
+47
View File
@@ -0,0 +1,47 @@
#include "db/transaction.hpp"
#include "db/sqllite_db.hpp"
#include <utility>
Transaction::Transaction(SqlliteDb &db)
: db_(&db) {
db_->execute("BEGIN TRANSACTION;");
}
Transaction::~Transaction() {
if(db_ != nullptr && not isCommited_) {
try {
db_->execute("ROLLBACK;");
} catch(...) {
// TODO
}
}
}
Transaction::Transaction(Transaction &&other) noexcept
: db_(std::exchange(other.db_, nullptr))
, isCommited_(std::exchange(other.isCommited_, false)) {}
Transaction &Transaction::operator=(Transaction &&other) noexcept {
if(this != &other) {
if(db_ != nullptr && not isCommited_) {
try {
db_->execute("ROLLBACK;");
} catch(...) {
// TODO some log
}
}
db_ = std::exchange(other.db_, nullptr);
isCommited_ = std::exchange(other.isCommited_, false);
}
return *this;
}
void Transaction::commit() {
if(db_ == nullptr || isCommited_) {
return;
}
db_->execute("COMMIT;");
isCommited_ = true;
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
class SqlliteDb;
class Transaction {
public:
explicit Transaction(SqlliteDb &db);
~Transaction();
Transaction(const Transaction &) = delete;
Transaction &operator=(const Transaction &) = delete;
Transaction(Transaction &&other) noexcept;
Transaction &operator=(Transaction &&other) noexcept;
void commit();
private:
SqlliteDb *db_{ nullptr };
bool isCommited_{ false };
};
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
#include <string>
struct User {
std::int64_t id{};
std::string email;
std::string password_hash;
bool is_active{ true };
std::string created_at;
std::string updated_at;
};
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <string>
struct RegisterRequest {
std::string mail;
std::string username;
std::string password;
};
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <cstdint>
#include <string>
struct UserResponce {
std::int64_t id{};
std::string email;
std::string username;
};
+72
View File
@@ -0,0 +1,72 @@
#include "repo/user_repository.hpp"
// #include "db/sqllite_db.hpp"
#include "db/statement.hpp"
#include <stdexcept>
namespace {
User read_user(Statement &stmt) {
User user;
user.id = stmt.column_int64(0);
user.email = stmt.column_text(1);
user.password_hash = stmt.column_text(2);
user.is_active = stmt.column_int64(3) != 0;
user.created_at = stmt.column_text(4);
user.updated_at = stmt.column_text(5);
return user;
}
} // namespace
UserRepository::UserRepository(SqlliteDb &db)
: db_(db) {}
std::optional<User> UserRepository::find_by_email(std::string_view email) const {
auto stmt = db_.prepare("SELECT id, email, password_hash, is_active, created_at, updated_at "
"FROM users "
"WHERE email = ?1 "
"LIMIT 1;");
stmt.bind_text(1, email);
if(!stmt.step()) {
return std::nullopt;
}
return read_user(stmt);
}
std::optional<User> UserRepository::find_by_id(std::int64_t id) const {
auto stmt = db_.prepare("SELECT id, email, password_hash, is_active, created_at, updated_at "
"FROM users "
"WHERE id = ?1 "
"LIMIT 1;");
stmt.bind_int64(1, id);
if(!stmt.step()) {
return std::nullopt;
}
return read_user(stmt);
}
User UserRepository::create(std::string_view email, std::string_view password_hash,
std::string_view created_at, std::string_view updated_at) const {
auto stmt =
db_.prepare("INSERT INTO users(email, password_hash, is_active, created_at, updated_at) "
"VALUES(?1, ?2, 1, ?3, ?4);");
stmt.bind_text(1, email);
stmt.bind_text(2, password_hash);
stmt.bind_text(3, created_at);
stmt.bind_text(4, updated_at);
stmt.execute();
const auto user_id = db_.last_insert_rowid();
const auto user = find_by_id(user_id);
if(!user.has_value()) {
throw std::runtime_error("Failed to load created user");
}
return *user;
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "db/sqllite_db.hpp"
#include "domain/User.h"
#include <cstdint>
#include <optional>
#include <string_view>
// class SqliteDb;
class UserRepository {
public:
explicit UserRepository(SqlliteDb &db);
std::optional<User> find_by_email(std::string_view email) const;
std::optional<User> find_by_id(std::int64_t id) const;
User create(std::string_view email, std::string_view password_hash, std::string_view created_at,
std::string_view updated_at) const;
private:
SqlliteDb &db_;
};