add database migration and UserRepository wrapper
- implemented DB migration - added UserRepository for DB interactions - inserted initial test record
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
build/
|
||||
|
||||
# ---> C++
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
@@ -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
Binary file not shown.
BIN
Binary file not shown.
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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_);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
struct RegisterRequest {
|
||||
std::string mail;
|
||||
std::string username;
|
||||
std::string password;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
struct UserResponce {
|
||||
std::int64_t id{};
|
||||
std::string email;
|
||||
std::string username;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
Reference in New Issue
Block a user