refactor postgres repo into multiple files

This commit is contained in:
2025-07-22 18:21:36 +02:00
parent 39676fe94a
commit 2ed69ae498
3 changed files with 506 additions and 485 deletions

View File

@@ -1,18 +1,13 @@
use std::str::FromStr; use anyhow::{Context as _, anyhow};
use anyhow::{Context, anyhow};
use argon2::{ use argon2::{
Argon2, PasswordHash, PasswordVerifier, Argon2, PasswordHash, PasswordVerifier as _,
password_hash::{ password_hash::{
PasswordHasher, SaltString, PasswordHasher as _, SaltString,
rand_core::{OsRng, RngCore as _}, rand_core::{OsRng, RngCore as _},
}, },
}; };
use chrono::Utc; use chrono::Utc;
use sqlx::{ use sqlx::{Acquire as _, PgConnection};
ConnectOptions as _, Connection as _, PgConnection, PgPool,
postgres::{PgConnectOptions, PgPoolOptions},
};
use uuid::Uuid; use uuid::Uuid;
use crate::domain::warren::{ use crate::domain::warren::{
@@ -39,128 +34,312 @@ use crate::domain::warren::{
FetchUserWarrensRequest, ListUserWarrensError, ListUserWarrensRequest, FetchUserWarrensRequest, ListUserWarrensError, ListUserWarrensRequest,
}, },
}, },
warren::{ warren::ListWarrensRequest,
FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest,
ListWarrensError, ListWarrensRequest, Warren,
},
}, },
ports::{AuthRepository, WarrenRepository, WarrenService}, ports::{AuthRepository, WarrenService},
}; };
#[derive(Debug, Clone)] use super::{Postgres, is_not_found_error};
pub struct PostgresConfig {
database_url: String,
database_name: String,
}
impl PostgresConfig { impl AuthRepository for Postgres {
pub fn new(database_url: String, database_name: String) -> Self { async fn create_user(&self, request: CreateUserRequest) -> Result<User, CreateUserError> {
Self { let mut connection = self
database_url, .pool
database_name, .acquire()
} .await
.context("Failed to get a PostgreSQL connection")?;
let user = self
.create_user(
&mut connection,
request.name(),
request.email(),
request.password(),
request.admin(),
)
.await
.context(format!("Failed to create user"))?;
Ok(user)
} }
}
#[derive(Debug, Clone)] async fn edit_user(&self, request: EditUserRequest) -> Result<User, EditUserError> {
pub struct Postgres { let mut connection = self
pool: PgPool, .pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user = self
.edit_user(
&mut connection,
request.user_id(),
request.name(),
request.email(),
request.password(),
request.admin(),
)
.await
.context(format!("Failed to edit user"))?;
Ok(user)
}
async fn delete_user(&self, request: DeleteUserRequest) -> Result<User, DeleteUserError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
self.delete_user_from_database(&mut connection, request.user_id())
.await
.map_err(|e| {
if is_not_found_error(&e) {
DeleteUserError::NotFound
} else {
DeleteUserError::Unknown(anyhow!(e))
}
})
}
async fn verify_user_password(
&self,
request: VerifyUserPasswordRequest,
) -> Result<User, VerifyUserPasswordError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user = self
.get_user_from_email(&mut connection, request.email())
.await
.map_err(|e| {
if is_not_found_error(&e) {
VerifyUserPasswordError::NotFound(request.email().clone())
} else {
VerifyUserPasswordError::Unknown(anyhow!(e))
}
})?;
self.check_user_password_against_hash(request.password(), user.password_hash())?;
Ok(user)
}
async fn create_auth_session(
&self,
request: CreateAuthSessionRequest,
) -> Result<AuthSession, CreateAuthSessionError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let session = self
.create_session(&mut connection, request.user(), request.expiration())
.await
.context("Failed to create session")?;
Ok(session)
}
async fn fetch_auth_session(
&self,
request: FetchAuthSessionRequest,
) -> Result<FetchAuthSessionResponse, FetchAuthSessionError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let session = self
.get_auth_session(&mut connection, request.session_id())
.await
.map_err(|e| {
if is_not_found_error(&e) {
FetchAuthSessionError::NotFound
} else {
anyhow!("Failed to get auth session: {e:?}").into()
}
})?;
let user = self
.get_user_from_id(&mut connection, session.user_id())
.await
.context("Failed to get user")?;
Ok(FetchAuthSessionResponse::new(session, user))
}
async fn create_user_warren(
&self,
request: CreateUserWarrenRequest,
) -> Result<UserWarren, CreateUserWarrenError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user_warren = self
.add_user_to_warren(&mut connection, request.user_warren())
.await
.context("Failed to create user warren")?;
Ok(user_warren)
}
async fn edit_user_warren(
&self,
request: EditUserWarrenRequest,
) -> Result<UserWarren, EditUserWarrenError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user_warren = self
.update_user_warren(&mut connection, request.user_warren())
.await
.context("Failed to edit user warren")?;
Ok(user_warren)
}
async fn delete_user_warren(
&self,
request: DeleteUserWarrenRequest,
) -> Result<UserWarren, DeleteUserWarrenError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user_warren = self
.remove_user_from_warren(&mut connection, request.user_id(), request.warren_id())
.await
.map_err(|e| {
if is_not_found_error(&e) {
DeleteUserWarrenError::NotFound
} else {
anyhow!("Failed to delete user warren: {e:?}").into()
}
})?;
Ok(user_warren)
}
async fn fetch_user_warrens(
&self,
request: FetchUserWarrensRequest,
) -> Result<Vec<UserWarren>, FetchUserWarrensError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user_warrens = self
.get_user_warrens(&mut connection, request.user_id())
.await
.context("Failed to get user warrens")?;
Ok(user_warrens)
}
async fn list_user_warrens(
&self,
_request: ListUserWarrensRequest,
) -> Result<Vec<UserWarren>, ListUserWarrensError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user_warrens = self
.get_all_user_warrens(&mut connection)
.await
.context("Failed to get all user warrens")?;
Ok(user_warrens)
}
async fn fetch_user_warren(
&self,
request: FetchUserWarrenRequest,
) -> Result<UserWarren, FetchUserWarrenError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
self.get_user_warren(&mut connection, request.user_id(), request.warren_id())
.await
.map_err(|e| {
if is_not_found_error(&e) {
FetchUserWarrenError::NotFound
} else {
FetchUserWarrenError::Unknown(anyhow!(e))
}
})
}
async fn list_users(&self, _request: ListUsersRequest) -> Result<Vec<User>, ListUsersError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let users = self
.fetch_users(&mut connection)
.await
.map_err(|e| anyhow!(e))?;
Ok(users)
}
async fn list_all_users_and_warrens<WS: WarrenService>(
&self,
_request: ListAllUsersAndWarrensRequest,
warren_service: &WS,
) -> Result<ListAllUsersAndWarrensResponse, ListAllUsersAndWarrensError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let users = self
.fetch_users(&mut connection)
.await
.context("Failed to fetch all users")?;
let user_warrens = self
.get_all_user_warrens(&mut connection)
.await
.context("Failed to fetch all user warrens")?;
let warrens = warren_service
.list_warrens(ListWarrensRequest::new())
.await
.context("Failed to get all warrens")?;
Ok(ListAllUsersAndWarrensResponse::new(
users,
user_warrens,
warrens,
))
}
} }
impl Postgres { impl Postgres {
pub async fn new(config: PostgresConfig) -> anyhow::Result<Self> {
let opts = PgConnectOptions::from_str(&config.database_url)?.disable_statement_logging();
let mut connection = PgConnection::connect_with(&opts)
.await
.context("Failed to connect to the PostgreSQL database")?;
match sqlx::query("SELECT datname FROM pg_database WHERE datname = $1")
.bind(&config.database_name)
.fetch_one(&mut connection)
.await
{
Ok(_) => (),
Err(sqlx::Error::RowNotFound) => {
sqlx::query(&format!("CREATE DATABASE {}", config.database_name))
.execute(&mut connection)
.await?;
}
Err(e) => return Err(e.into()),
};
connection.close().await?;
let pool = PgPoolOptions::new()
.connect_with(opts.database(&config.database_name))
.await?;
sqlx::migrate!("./migrations").run(&pool).await?;
Ok(Self { pool })
}
async fn get_warren(
&self,
connection: &mut PgConnection,
id: &Uuid,
) -> Result<Warren, sqlx::Error> {
let warren: Warren = sqlx::query_as(
"
SELECT
*
FROM
warrens
WHERE
id = $1
",
)
.bind(id)
.fetch_one(connection)
.await?;
Ok(warren)
}
async fn fetch_warrens(
&self,
connection: &mut PgConnection,
ids: &[Uuid],
) -> Result<Vec<Warren>, sqlx::Error> {
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
"
SELECT
*
FROM
warrens
WHERE
id = ANY($1)
",
)
.bind(ids)
.fetch_all(&mut *connection)
.await?;
Ok(warrens)
}
async fn fetch_all_warrens(
&self,
connection: &mut PgConnection,
) -> Result<Vec<Warren>, sqlx::Error> {
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
"
SELECT
*
FROM
warrens
",
)
.fetch_all(&mut *connection)
.await?;
Ok(warrens)
}
async fn create_user( async fn create_user(
&self, &self,
connection: &mut PgConnection, connection: &mut PgConnection,
@@ -602,369 +781,6 @@ impl Postgres {
} }
} }
impl WarrenRepository for Postgres {
async fn fetch_warrens(
&self,
request: FetchWarrensRequest,
) -> Result<Vec<Warren>, FetchWarrensError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let warrens = self
.fetch_warrens(&mut connection, request.ids())
.await
.map_err(|err| anyhow!(err).context("Failed to fetch warrens"))?;
Ok(warrens)
}
async fn list_warrens(
&self,
_request: ListWarrensRequest,
) -> Result<Vec<Warren>, ListWarrensError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let warrens = self
.fetch_all_warrens(&mut connection)
.await
.map_err(|err| anyhow!(err).context("Failed to list all warrens"))?;
Ok(warrens)
}
async fn fetch_warren(&self, request: FetchWarrenRequest) -> Result<Warren, FetchWarrenError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let warren = self
.get_warren(&mut connection, request.id())
.await
.map_err(|err| {
if is_not_found_error(&err) {
return FetchWarrenError::NotFound(request.id().clone());
}
anyhow!(err)
.context(format!("Failed to fetch warren with id {:?}", request.id()))
.into()
})?;
Ok(warren)
}
}
impl AuthRepository for Postgres {
async fn create_user(&self, request: CreateUserRequest) -> Result<User, CreateUserError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user = self
.create_user(
&mut connection,
request.name(),
request.email(),
request.password(),
request.admin(),
)
.await
.context(format!("Failed to create user"))?;
Ok(user)
}
async fn edit_user(&self, request: EditUserRequest) -> Result<User, EditUserError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user = self
.edit_user(
&mut connection,
request.user_id(),
request.name(),
request.email(),
request.password(),
request.admin(),
)
.await
.context(format!("Failed to edit user"))?;
Ok(user)
}
async fn delete_user(&self, request: DeleteUserRequest) -> Result<User, DeleteUserError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
self.delete_user_from_database(&mut connection, request.user_id())
.await
.map_err(|e| {
if is_not_found_error(&e) {
DeleteUserError::NotFound
} else {
DeleteUserError::Unknown(anyhow!(e))
}
})
}
async fn verify_user_password(
&self,
request: VerifyUserPasswordRequest,
) -> Result<User, VerifyUserPasswordError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user = self
.get_user_from_email(&mut connection, request.email())
.await
.map_err(|e| {
if is_not_found_error(&e) {
VerifyUserPasswordError::NotFound(request.email().clone())
} else {
VerifyUserPasswordError::Unknown(anyhow!(e))
}
})?;
self.check_user_password_against_hash(request.password(), user.password_hash())?;
Ok(user)
}
async fn create_auth_session(
&self,
request: CreateAuthSessionRequest,
) -> Result<AuthSession, CreateAuthSessionError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let session = self
.create_session(&mut connection, request.user(), request.expiration())
.await
.context("Failed to create session")?;
Ok(session)
}
async fn fetch_auth_session(
&self,
request: FetchAuthSessionRequest,
) -> Result<FetchAuthSessionResponse, FetchAuthSessionError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let session = self
.get_auth_session(&mut connection, request.session_id())
.await
.map_err(|e| {
if is_not_found_error(&e) {
FetchAuthSessionError::NotFound
} else {
anyhow!("Failed to get auth session: {e:?}").into()
}
})?;
let user = self
.get_user_from_id(&mut connection, session.user_id())
.await
.context("Failed to get user")?;
Ok(FetchAuthSessionResponse::new(session, user))
}
async fn create_user_warren(
&self,
request: CreateUserWarrenRequest,
) -> Result<UserWarren, CreateUserWarrenError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user_warren = self
.add_user_to_warren(&mut connection, request.user_warren())
.await
.context("Failed to create user warren")?;
Ok(user_warren)
}
async fn edit_user_warren(
&self,
request: EditUserWarrenRequest,
) -> Result<UserWarren, EditUserWarrenError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user_warren = self
.update_user_warren(&mut connection, request.user_warren())
.await
.context("Failed to edit user warren")?;
Ok(user_warren)
}
async fn delete_user_warren(
&self,
request: DeleteUserWarrenRequest,
) -> Result<UserWarren, DeleteUserWarrenError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user_warren = self
.remove_user_from_warren(&mut connection, request.user_id(), request.warren_id())
.await
.map_err(|e| {
if is_not_found_error(&e) {
DeleteUserWarrenError::NotFound
} else {
anyhow!("Failed to delete user warren: {e:?}").into()
}
})?;
Ok(user_warren)
}
async fn fetch_user_warrens(
&self,
request: FetchUserWarrensRequest,
) -> Result<Vec<UserWarren>, FetchUserWarrensError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user_warrens = self
.get_user_warrens(&mut connection, request.user_id())
.await
.context("Failed to get user warrens")?;
Ok(user_warrens)
}
async fn list_user_warrens(
&self,
_request: ListUserWarrensRequest,
) -> Result<Vec<UserWarren>, ListUserWarrensError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user_warrens = self
.get_all_user_warrens(&mut connection)
.await
.context("Failed to get all user warrens")?;
Ok(user_warrens)
}
async fn fetch_user_warren(
&self,
request: FetchUserWarrenRequest,
) -> Result<UserWarren, FetchUserWarrenError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
self.get_user_warren(&mut connection, request.user_id(), request.warren_id())
.await
.map_err(|e| {
if is_not_found_error(&e) {
FetchUserWarrenError::NotFound
} else {
FetchUserWarrenError::Unknown(anyhow!(e))
}
})
}
async fn list_users(&self, _request: ListUsersRequest) -> Result<Vec<User>, ListUsersError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let users = self
.fetch_users(&mut connection)
.await
.map_err(|e| anyhow!(e))?;
Ok(users)
}
async fn list_all_users_and_warrens<WS: WarrenService>(
&self,
_request: ListAllUsersAndWarrensRequest,
warren_service: &WS,
) -> Result<ListAllUsersAndWarrensResponse, ListAllUsersAndWarrensError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let users = self
.fetch_users(&mut connection)
.await
.context("Failed to fetch all users")?;
let user_warrens = self
.get_all_user_warrens(&mut connection)
.await
.context("Failed to fetch all user warrens")?;
let warrens = warren_service
.list_warrens(ListWarrensRequest::new())
.await
.context("Failed to get all warrens")?;
Ok(ListAllUsersAndWarrensResponse::new(
users,
user_warrens,
warrens,
))
}
}
fn is_not_found_error(err: &sqlx::Error) -> bool {
matches!(err, sqlx::Error::RowNotFound)
}
fn hash_password(password: &UserPassword) -> Result<String, argon2::password_hash::Error> { fn hash_password(password: &UserPassword) -> Result<String, argon2::password_hash::Error> {
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default(); let argon2 = Argon2::default();

View File

@@ -0,0 +1,67 @@
use std::str::FromStr as _;
use anyhow::Context as _;
use sqlx::{
ConnectOptions as _, Connection as _, PgConnection, PgPool,
postgres::{PgConnectOptions, PgPoolOptions},
};
pub mod auth;
pub mod warrens;
#[derive(Debug, Clone)]
pub struct PostgresConfig {
database_url: String,
database_name: String,
}
impl PostgresConfig {
pub fn new(database_url: String, database_name: String) -> Self {
Self {
database_url,
database_name,
}
}
}
#[derive(Debug, Clone)]
pub struct Postgres {
pool: PgPool,
}
impl Postgres {
pub async fn new(config: PostgresConfig) -> anyhow::Result<Self> {
let opts = PgConnectOptions::from_str(&config.database_url)?.disable_statement_logging();
let mut connection = PgConnection::connect_with(&opts)
.await
.context("Failed to connect to the PostgreSQL database")?;
match sqlx::query("SELECT datname FROM pg_database WHERE datname = $1")
.bind(&config.database_name)
.fetch_one(&mut connection)
.await
{
Ok(_) => (),
Err(sqlx::Error::RowNotFound) => {
sqlx::query(&format!("CREATE DATABASE {}", config.database_name))
.execute(&mut connection)
.await?;
}
Err(e) => return Err(e.into()),
};
connection.close().await?;
let pool = PgPoolOptions::new()
.connect_with(opts.database(&config.database_name))
.await?;
sqlx::migrate!("./migrations").run(&pool).await?;
Ok(Self { pool })
}
}
pub(super) fn is_not_found_error(err: &sqlx::Error) -> bool {
matches!(err, sqlx::Error::RowNotFound)
}

View File

@@ -0,0 +1,138 @@
use anyhow::{Context as _, anyhow};
use sqlx::PgConnection;
use uuid::Uuid;
use crate::domain::warren::{
models::warren::{
FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest,
ListWarrensError, ListWarrensRequest, Warren,
},
ports::WarrenRepository,
};
use super::{Postgres, is_not_found_error};
impl WarrenRepository for Postgres {
async fn fetch_warrens(
&self,
request: FetchWarrensRequest,
) -> Result<Vec<Warren>, FetchWarrensError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let warrens = self
.fetch_warrens(&mut connection, request.ids())
.await
.map_err(|err| anyhow!(err).context("Failed to fetch warrens"))?;
Ok(warrens)
}
async fn list_warrens(
&self,
_request: ListWarrensRequest,
) -> Result<Vec<Warren>, ListWarrensError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let warrens = self
.fetch_all_warrens(&mut connection)
.await
.map_err(|err| anyhow!(err).context("Failed to list all warrens"))?;
Ok(warrens)
}
async fn fetch_warren(&self, request: FetchWarrenRequest) -> Result<Warren, FetchWarrenError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let warren = self
.get_warren(&mut connection, request.id())
.await
.map_err(|err| {
if is_not_found_error(&err) {
return FetchWarrenError::NotFound(request.id().clone());
}
anyhow!(err)
.context(format!("Failed to fetch warren with id {:?}", request.id()))
.into()
})?;
Ok(warren)
}
}
impl Postgres {
async fn get_warren(
&self,
connection: &mut PgConnection,
id: &Uuid,
) -> Result<Warren, sqlx::Error> {
let warren: Warren = sqlx::query_as(
"
SELECT
*
FROM
warrens
WHERE
id = $1
",
)
.bind(id)
.fetch_one(connection)
.await?;
Ok(warren)
}
async fn fetch_warrens(
&self,
connection: &mut PgConnection,
ids: &[Uuid],
) -> Result<Vec<Warren>, sqlx::Error> {
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
"
SELECT
*
FROM
warrens
WHERE
id = ANY($1)
",
)
.bind(ids)
.fetch_all(&mut *connection)
.await?;
Ok(warrens)
}
async fn fetch_all_warrens(
&self,
connection: &mut PgConnection,
) -> Result<Vec<Warren>, sqlx::Error> {
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
"
SELECT
*
FROM
warrens
",
)
.fetch_all(&mut *connection)
.await?;
Ok(warrens)
}
}