From fe9fb77a02c3b5f4e0d04e339180f477303f6f8d Mon Sep 17 00:00:00 2001 From: ANTON IGNATEV Date: Thu, 9 Apr 2026 08:00:44 +0300 Subject: [PATCH] add database migration and UserRepository wrapper - implemented DB migration - added UserRepository for DB interactions - inserted initial test record --- .gitignore | 2 + CMakeLists.txt | 3 ++ auth.db | Bin 0 -> 16384 bytes auth.db-shm | Bin 0 -> 32768 bytes auth.db-wal | Bin 0 -> 90672 bytes migrations/001_init.sql | 30 +++++++++++++ src/application/application.cpp | 36 +++++++--------- src/db/migrations_runner.cpp | 22 ++++++++++ src/db/migrations_runner.hpp | 14 +++++++ src/db/sqllite_db.cpp | 8 ++++ src/db/sqllite_db.hpp | 7 +++- src/db/transaction.cpp | 47 +++++++++++++++++++++ src/db/transaction.hpp | 21 ++++++++++ src/domain/User.h | 13 ++++++ src/modeles/RegisterRequest.h | 9 ++++ src/modeles/UserResponse.h | 10 +++++ src/repo/user_repository.cpp | 72 ++++++++++++++++++++++++++++++++ src/repo/user_repository.hpp | 24 +++++++++++ 18 files changed, 294 insertions(+), 24 deletions(-) create mode 100644 auth.db create mode 100644 auth.db-shm create mode 100644 auth.db-wal create mode 100644 migrations/001_init.sql create mode 100644 src/db/migrations_runner.cpp create mode 100644 src/db/migrations_runner.hpp create mode 100644 src/db/transaction.cpp create mode 100644 src/db/transaction.hpp create mode 100644 src/domain/User.h create mode 100644 src/modeles/RegisterRequest.h create mode 100644 src/modeles/UserResponse.h create mode 100644 src/repo/user_repository.cpp create mode 100644 src/repo/user_repository.hpp diff --git a/.gitignore b/.gitignore index ee4d4c8..89385f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +build/ + # ---> C++ # Prerequisites *.d diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bcab0a..dc41378 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/auth.db b/auth.db new file mode 100644 index 0000000000000000000000000000000000000000..6381f6b1921e3c56f66f158980f3d371bfd4f58b GIT binary patch literal 16384 zcmeI&!A`<37zgmKB`z9uCdP2!V2=>ci$^a`vl!wSI7cEUGDZ!AjfoUu;)$d0yZ9mUB|jm1gi>he( zudEg~n_j)@s3A4W@hW`k zhZDg)>(=8=U6rP7ml|{!ge+X@wIG@xbU*{t0u?wUBqb!E1w;be;6*}YNEi6H zH_~cWtJU1>yL_tWMP&5WYkFGe-CXB)k5{Yv`RnBJ($vy)SRBt3TeaOMOMw z{rC~(IF8FEzdx*3=IxS4CGV6xE;;+ld;NNC{n`)8S=P!rf&c*m1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZV3P%U_56rojA9&naS+o@ZdGpt8WuQ=S;G~TBhVIsNgTzzEzRnaK%)ZRbGI5T zsu+P*2=wEm6@6-wK=T5F$Z3p%Kon zZ&%gNcXW4TEkoTkG@92f^)j(_O^tG`s*3m4iHBEiQ!2IMS=Kql-k-BD%X6KwZv6fR zv9hk=pc38_jx9eRUQiG~009ILKmY**5I_I{1QsGNIbP?FL?WJj+bsQ-oH1q^qo)1K zyE&<~r8G6wc3p?2+Er>aq>7TwsPRNfYuA!$S2Di6E!n4T)B4o5o>XT%A-3ACB~okc zcN%#;n^RL-Z%R#cibqdJM{&ilZko3Zjb_pVx;Ze@8?t6v@3*q!#?+yl9jIHht!+IW zDRq5uSN)@gZW)=hxB#8uKx4z18P|qlgSGxhQ~D^^Aedfr$owv0{Ic>Hpg zlbh^)4j6jQ8t5M|`Ulfiez;<>#Vj^`OfCCTSi52q1s}0tg_000IagfB*t#w16vKprf(*-Vfebv44(u0e_j8%tKgk zynt_6S6RG(SfC()00IagfB*srAb`M<7r3*@=Z~yj?YS#hOvE-!Gdol;&C;{ql_u@f zo^aN?>|0Nl^^rQ6>&1L^YiQ6Y%$oQ9-`VWO#Bg@h5UKGq=DLp><6_@anefHKZS72I z@%Dtw(2tf*ET$&4ty)q`Y}R&^`u4M#SSUukfE=KJ@dXyu_yY18-|xf=bYFPUi-#`w z@K(kbSn^-RGynkv5I_I{1Q0*~0R#|00D<`kxZ(x$l}A=Ru;#tU9Pt8yvK`AjgayY7 z`0J0l;swMa1px#QKmY**5I_I{1WvBNkl!EKwAwS-IF-@v^wFoMd)1w)z--Cw&Kt_M zkxA}Ob#!`HG2#VeAOrCNV$K8RBe3UzSoHG|$ZPz76EAS=(iM+9_Q|(d#uqrbU&QPU z0R#|0009ILKmY**5I_KdMJnKm7x=jK$z2bg{ptES;st_bZIO8h3yv2E)bFi`7Z4Q` z1Q0*~0R#|0009KfEPzR1Sap54cH=a(|ooj&bkobNU%uM-!W{7e_HBcbn9M_1aJ)dU{+#3D1w<7E0R#|0009IL zKmdW$FE9`+X61X2@5Y}Vuioje56#|_KmBG`Xa00`jCcV#TLJSC=-HeM@i2X}4e?;F zP20Wq^0QBZL_CpM&6P6}kK*XDfITT`DHNHr5 zv!|~(&BjPBYZ+-W~5L%e_xr67O+0tg_000Ib{egWbI zx_dN{fxboS6{+YGqF>ZBWxqc25g6k`gGMH;TXxEQG1Go)XHtu|CuD|xRCdZUA3;bJ zh4BUE7+*kq&)t3czw3(E_lgrQ@c1`}S3ZBm6T26~iE$1HAbe0c!bfGb`g)PCQN>k|)k%Xk4#_%|i|8{-a^S@9A91Q0*~0R#|0 z009ILKmdU%5D0i`BSAT*rYRcZl?`&{DO~f=`#<+R^YBRVzQF5B`1LAiE=@oH0R#|0 z009ILKmY**5I~@s1mYgQ(sFTd<3>5nz_rGNo*&K`>-vZCnX!C+cX~iK2R5{BxT2-? z@|M<(sr4JKX>AohH_TKgSbX9!l}B*sl{KN2uYP!ooMgZgKBkBt3IYfqfB*srAb +#include "db/migrations_runner.hpp" #include "db/sqllite_db.hpp" #include "http/controllers/health_controller.hpp" +#include "repo/user_repository.hpp" #include #include @@ -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); diff --git a/src/db/migrations_runner.cpp b/src/db/migrations_runner.cpp new file mode 100644 index 0000000..8143807 --- /dev/null +++ b/src/db/migrations_runner.cpp @@ -0,0 +1,22 @@ +#include "db/migrations_runner.hpp" + +#include "db/sqllite_db.hpp" + +#include +#include +#include + +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()); +} \ No newline at end of file diff --git a/src/db/migrations_runner.hpp b/src/db/migrations_runner.hpp new file mode 100644 index 0000000..78b5b0c --- /dev/null +++ b/src/db/migrations_runner.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +class SqlliteDb; + +class MigrationsRunner { +public: + explicit MigrationsRunner(SqlliteDb &db); + void run_file(const std::string &path); + +private: + SqlliteDb &db_; +}; \ No newline at end of file diff --git a/src/db/sqllite_db.cpp b/src/db/sqllite_db.cpp index d9e5d17..99182ad 100644 --- a/src/db/sqllite_db.cpp +++ b/src/db/sqllite_db.cpp @@ -69,4 +69,12 @@ void SqlliteDb::closeDb() { sqlite3_close(db_); db_ = nullptr; } +} + +Transaction SqlliteDb::begin_transaction(){ + return Transaction{*this}; +} + +std::int64_t SqlliteDb::last_insert_rowid() const noexcept{ + return sqlite3_last_insert_rowid(db_); } \ No newline at end of file diff --git a/src/db/sqllite_db.hpp b/src/db/sqllite_db.hpp index dae2d41..a60a102 100644 --- a/src/db/sqllite_db.hpp +++ b/src/db/sqllite_db.hpp @@ -1,6 +1,7 @@ #pragma once #include "db/statement.hpp" +#include "db/transaction.hpp" #include #include @@ -20,9 +21,11 @@ public: void execute(std::string_view sql); void initialize(); - Statement prepare(std::string_view sql) const; + Statement prepare(std::string_view sql) const; + Transaction begin_transaction(); - sqlite3 *native_handle() const noexcept; + sqlite3 *native_handle() const noexcept; + std::int64_t last_insert_rowid() const noexcept; private: void closeDb(); diff --git a/src/db/transaction.cpp b/src/db/transaction.cpp new file mode 100644 index 0000000..abee603 --- /dev/null +++ b/src/db/transaction.cpp @@ -0,0 +1,47 @@ +#include "db/transaction.hpp" + +#include "db/sqllite_db.hpp" + +#include + +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; +} \ No newline at end of file diff --git a/src/db/transaction.hpp b/src/db/transaction.hpp new file mode 100644 index 0000000..50f326a --- /dev/null +++ b/src/db/transaction.hpp @@ -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 }; +}; \ No newline at end of file diff --git a/src/domain/User.h b/src/domain/User.h new file mode 100644 index 0000000..1703c6a --- /dev/null +++ b/src/domain/User.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +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; +}; diff --git a/src/modeles/RegisterRequest.h b/src/modeles/RegisterRequest.h new file mode 100644 index 0000000..1e23125 --- /dev/null +++ b/src/modeles/RegisterRequest.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +struct RegisterRequest { + std::string mail; + std::string username; + std::string password; +}; \ No newline at end of file diff --git a/src/modeles/UserResponse.h b/src/modeles/UserResponse.h new file mode 100644 index 0000000..60a67d6 --- /dev/null +++ b/src/modeles/UserResponse.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +struct UserResponce { + std::int64_t id{}; + std::string email; + std::string username; +}; \ No newline at end of file diff --git a/src/repo/user_repository.cpp b/src/repo/user_repository.cpp new file mode 100644 index 0000000..47fcb10 --- /dev/null +++ b/src/repo/user_repository.cpp @@ -0,0 +1,72 @@ +#include "repo/user_repository.hpp" + +// #include "db/sqllite_db.hpp" +#include "db/statement.hpp" + +#include + +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 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 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; +} \ No newline at end of file diff --git a/src/repo/user_repository.hpp b/src/repo/user_repository.hpp new file mode 100644 index 0000000..767fc01 --- /dev/null +++ b/src/repo/user_repository.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "db/sqllite_db.hpp" +#include "domain/User.h" + +#include +#include +#include + +// class SqliteDb; + +class UserRepository { +public: + explicit UserRepository(SqlliteDb &db); + + std::optional find_by_email(std::string_view email) const; + std::optional 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_; +}; \ No newline at end of file