diff --git a/Cargo.toml b/Cargo.toml index a250128..fdcc3c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = ["crates/taom-database", "crates/taom-database-macro"] + [dependencies] actix-governor = "0.5" actix-web = "4.9" @@ -26,6 +29,7 @@ semver = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = { version = "3.9", features = ["base64", "time_0_3"] } +taom-database = { path = "./crates/taom-database" } tokio = "1.39" tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1"] } url = "2.5" diff --git a/crates/taom-database-macro/Cargo.toml b/crates/taom-database-macro/Cargo.toml new file mode 100644 index 0000000..71f88e5 --- /dev/null +++ b/crates/taom-database-macro/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "taom-database-macro" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2", features = ["full", "parsing"] } diff --git a/crates/taom-database-macro/src/attribute.rs b/crates/taom-database-macro/src/attribute.rs new file mode 100644 index 0000000..c98f59a --- /dev/null +++ b/crates/taom-database-macro/src/attribute.rs @@ -0,0 +1,58 @@ +use syn::{Attribute, LitStr, Result, Token}; + +macro_rules! fail { + ($t:expr, $m:expr) => { + return Err(syn::Error::new_spanned($t, $m)) + }; +} + +macro_rules! try_set { + (option : $i:expr, $v:expr, $t:expr) => { + match $i { + None => { + $i = Some($v); + Ok(()) + } + Some(_) => fail!($t, "duplicate attribute"), + } + }; + (bool : $i:expr, $t:expr) => { + match $i { + false => { + $i = true; + Ok(()) + } + true => fail!($t, "duplicate attribute"), + } + }; +} + +#[derive(Default)] +pub(crate) struct DbRowAttribute { + pub rename: Option, + pub default: bool, +} + +pub fn parse_db_row_attr(attrs: &[Attribute]) -> Result { + let mut result_attr = DbRowAttribute::default(); + + for attr in attrs.iter().filter(|attr| attr.path().is_ident("db_row")) { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename") { + meta.input.parse::()?; + let val: LitStr = meta.input.parse()?; + try_set!(option : result_attr.rename, val.value(), val) + } else if meta.path.is_ident("default") { + try_set!(bool : result_attr.default, meta.path) + } else { + let msg = match meta.path.get_ident() { + Some(ident) => format!("Unexpected attribute `{}` in db_row", ident), + None => "Unexpected attribute".to_string(), + }; + fail!(meta.path, msg) + } + })? + } + + Ok(result_attr) +} diff --git a/crates/taom-database-macro/src/lib.rs b/crates/taom-database-macro/src/lib.rs new file mode 100644 index 0000000..3063131 --- /dev/null +++ b/crates/taom-database-macro/src/lib.rs @@ -0,0 +1,64 @@ +use attribute::parse_db_row_attr; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput, Fields, Result}; + +mod attribute; + +#[proc_macro_derive(FromRow, attributes(db_row))] +pub fn derive_from_row(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let syn::Data::Struct(data) = input.data else { + return TokenStream::from( + syn::Error::new(input.ident.span(), "Only structs can derive `FromRow`") + .to_compile_error(), + ); + }; + + let struct_name = input.ident; + let fields = data + .fields + .iter() + .enumerate() + .map(|(i, field)| { + let attr = parse_db_row_attr(field.attrs.as_slice())?; + Ok(match field.ident.as_ref() { + Some(name) => { + let db_name = attr.rename.unwrap_or(name.to_string()); + match attr.default { + true => quote! { #name: row.try_get(#db_name).unwrap_or_default() }, + false => quote! { #name: row.try_get(#db_name)? }, + } + } + None => match (attr.rename, attr.default) { + (Some(db_name), true) => quote! { row.try_get(#db_name).unwrap_or_default() }, + (Some(db_name), false) => quote! { row.try_get(#db_name)? }, + (None, true) => quote! { row.try_get(#i).unwrap_or_default() }, + (None, false) => quote! { row.try_get(#i)? }, + } + }) + }) + .collect::>>(); + + let fields = match fields { + Ok(ts) => ts, + Err(e) => return e.to_compile_error().into(), + }; + + let struct_self = match data.fields { + Fields::Named(_) => quote! { Self {#(#fields),*} }, + Fields::Unnamed(_) => quote! { Self(#(#fields),*) }, + Fields::Unit => quote! { Self }, + }; + + quote! { + #[automatically_derived] + impl ::taom_database::FromRow for #struct_name { + fn from_row(row: ::tokio_postgres::Row) -> Result { + Ok(#struct_self) + } + } + } + .into() +} diff --git a/crates/taom-database/Cargo.toml b/crates/taom-database/Cargo.toml new file mode 100644 index 0000000..5c676a6 --- /dev/null +++ b/crates/taom-database/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "taom-database" +version = "0.1.0" +edition = "2021" + +[dependencies] +deadpool-postgres = "0.14" +futures = "0.3" +taom-database-macro = { path = "../taom-database-macro"} +tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1"] } diff --git a/crates/taom-database/src/config.rs b/crates/taom-database/src/config.rs new file mode 100644 index 0000000..41562fe --- /dev/null +++ b/crates/taom-database/src/config.rs @@ -0,0 +1,61 @@ +use deadpool_postgres::{Config, ManagerConfig, Pool, RecyclingMethod, Runtime}; +use tokio_postgres::NoTls; + +use crate::error::PoolError; + +#[derive(Default)] +pub struct ConfigBuilder<'b> { + host: Option<&'b str>, + password: Option<&'b str>, + user: Option<&'b str>, + database: Option<&'b str>, + recycling_method: RecyclingMethod, + runtime: Option, +} + +impl<'b> ConfigBuilder<'b> { + pub fn host(mut self, host: &'b str) -> Self { + self.host = Some(host); + self + } + pub fn password(mut self, password: &'b str) -> Self { + self.password = Some(password); + self + } + pub fn user(mut self, user: &'b str) -> Self { + self.user = Some(user); + self + } + pub fn database(mut self, database: &'b str) -> Self { + self.database = Some(database); + self + } + pub fn recycling_method(mut self, recycling_method: RecyclingMethod) -> Self { + self.recycling_method = recycling_method; + self + } + pub fn runtime(mut self, runtime: Runtime) -> Self { + self.runtime = Some(runtime); + self + } + + pub async fn build(self) -> Result { + let mut pg_config = Config::new(); + pg_config.host = self.host.map(str::to_string); + pg_config.password = self.password.map(str::to_string); + pg_config.user = self.user.map(str::to_string); + pg_config.dbname = self.database.map(str::to_string); + pg_config.manager = Some(ManagerConfig { + recycling_method: self.recycling_method, + }); + + let pool = pg_config + .create_pool(self.runtime, NoTls) + .map_err(|_| PoolError::Creation)?; + + // Try to connect to database to test if the database exist + let _ = pool.get().await.map_err(|_| PoolError::Connection)?; + + Ok(pool) + } +} diff --git a/crates/taom-database/src/error.rs b/crates/taom-database/src/error.rs new file mode 100644 index 0000000..f702486 --- /dev/null +++ b/crates/taom-database/src/error.rs @@ -0,0 +1,29 @@ +use core::fmt::Display; + +use tokio_postgres::Error; + +use crate::Query; + +pub enum PoolError { + Creation, + Connection, +} + +#[derive(Debug)] +pub enum QueryError { + PreparationFailed(Query<'static>), + HasMoreThanOneRow(/*first_row:*/ R), + ExecuteFailed(Error), + HasNoRow, +} + +impl Display for QueryError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::PreparationFailed(_) => write!(f, "failure during the preparation of the query"), + Self::HasMoreThanOneRow(_) => write!(f, "collect more than one result"), + Self::ExecuteFailed(err) => write!(f, "{err}"), + Self::HasNoRow => write!(f, "no row found"), + } + } +} diff --git a/crates/taom-database/src/from_row.rs b/crates/taom-database/src/from_row.rs new file mode 100644 index 0000000..0796c2f --- /dev/null +++ b/crates/taom-database/src/from_row.rs @@ -0,0 +1,224 @@ +#[doc(hidden)] +pub use taom_database_macro::FromRow; +use tokio_postgres::types::FromSql; +use tokio_postgres::{Error, Row}; + +/// Trait to transform a row into an object +pub trait FromRow: Sized { + fn from_row(row: Row) -> Result; +} + +impl FromRow for Row { + #[inline] + fn from_row(row: Row) -> Result { + Ok(row) + } +} + +macro_rules! impl_from_row_for_from_sql { + ($($type:ty),+) => { + $( + impl FromRow for $type { + #[inline] + fn from_row(row: Row) -> Result { + row.try_get(0) + } + } + )+ + }; +} + +impl_from_row_for_from_sql!(i8, i16, i32, i64, String); + +macro_rules! impl_from_row_for_tuple { + () => { + impl FromRow for () { + #[inline] + fn from_row(_: Row) -> Result { + Ok(()) + } + } + }; + ($($idx:literal -> $T:ident;)+) => { + impl<$($T,)+> FromRow for ($($T,)+) + where + $($T: for<'r> FromSql<'r>,)+ + { + #[inline] + fn from_row(row: Row) -> Result { + Ok(($(row.try_get($idx as usize)?,)+)) + } + } + }; +} + +impl_from_row_for_tuple! {} +impl_from_row_for_tuple! { + 0 -> T1; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; +} + +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; + 11 -> T12; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; + 11 -> T12; + 12 -> T13; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; + 11 -> T12; + 12 -> T13; + 13 -> T14; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; + 11 -> T12; + 12 -> T13; + 13 -> T14; + 14 -> T15; +} +impl_from_row_for_tuple! { + 0 -> T1; + 1 -> T2; + 2 -> T3; + 3 -> T4; + 4 -> T5; + 5 -> T6; + 6 -> T7; + 7 -> T8; + 8 -> T9; + 9 -> T10; + 10 -> T11; + 11 -> T12; + 12 -> T13; + 13 -> T14; + 14 -> T15; + 15 -> T16; +} diff --git a/crates/taom-database/src/lib.rs b/crates/taom-database/src/lib.rs new file mode 100644 index 0000000..4ff7c1b --- /dev/null +++ b/crates/taom-database/src/lib.rs @@ -0,0 +1,20 @@ +// allow to write `fn foo();` +// ^^^^^^^ +#![allow(invalid_type_param_default)] + +pub use from_row::FromRow; +pub use map::ConstQueryMap; +pub use query::Query; +use tokio_postgres::types::ToSql; + +pub mod config; +pub mod error; +mod from_row; +mod map; +mod prepare; +mod query; + +#[inline(always)] +pub fn dynamic<'a, T: ToSql + Sync>(v: &'a T) -> &'a (dyn ToSql + Sync) { + v +} diff --git a/crates/taom-database/src/map.rs b/crates/taom-database/src/map.rs new file mode 100644 index 0000000..2102c19 --- /dev/null +++ b/crates/taom-database/src/map.rs @@ -0,0 +1,38 @@ +use deadpool_postgres::Client; +use tokio_postgres::Row; + +use crate::prepare::Prepare; +use crate::FromRow; + +use super::query::Query; + +/// Constante map of all queries +pub struct ConstQueryMap([(K, Query<'static>); N]); + +unsafe impl Sync for ConstQueryMap {} + +impl ConstQueryMap { + pub const fn new(queries: [(K, Query<'static>); N]) -> Self { + Self(queries) + } + + /// Give Prepare object to prepare a query + pub fn prepare<'c, R: FromRow = Row>(&self, k: K, client: &'c Client) -> Prepare<'c, R> { + self.try_prepare::(k, client).expect("item should exist") + } + + /// Try to give Prepare object to prepare a query + pub fn try_prepare<'c, R: FromRow = Row>( + &self, + k: K, + client: &'c Client, + ) -> Option> { + for (key, query) in &self.0 { + if &k == key { + return Some(Prepare::new(client, query.to_owned())); + } + } + + None + } +} diff --git a/crates/taom-database/src/prepare.rs b/crates/taom-database/src/prepare.rs new file mode 100644 index 0000000..56b7636 --- /dev/null +++ b/crates/taom-database/src/prepare.rs @@ -0,0 +1,124 @@ +use core::marker::PhantomData; + +use deadpool_postgres::Client; +use futures::{pin_mut, Stream, StreamExt, TryStreamExt}; +use tokio_postgres::types::BorrowToSql; +use tokio_postgres::{Error, Statement}; + +use crate::error::QueryError; +use crate::{FromRow, Query}; + +/// Prepare and send the request and allow to change the row value of each result +/// from the database with its generic `R` +pub struct Prepare<'c, R> { + query: Query<'static>, + client: &'c Client, + _phantom: PhantomData, +} + +impl<'c, R: FromRow> Prepare<'c, R> { + pub fn new(client: &'c Client, query: Query<'static>) -> Self { + Self { + query, + client, + _phantom: PhantomData, + } + } + + /// Return a Stream (it's like an async iterator) of all the row get by + /// the query or a `PreparationFailed`. + pub async fn query_iter( + &self, + params: P, + ) -> Result>, QueryError> + where + I: BorrowToSql, + P: IntoIterator, + P::IntoIter: ExactSizeIterator, + { + let statement = self.prepare().await?; + let result = self + .client + .query_raw(&statement, params) + .await + .map_err(|_| QueryError::PreparationFailed(self.query.clone()))?; + + Ok(result.map(|row| R::from_row(row?))) + } + + /// Return the first result of the query. + /// If the query result has only one row, it will be stored in `Ok(Some)`, + /// if the query has no result, it will return `Ok(None)`, + /// and if the query result has more than one row, the first row will be + /// stored in `Err(QueryError::HasMoreThanOneRow)`. + pub async fn query_one(&self, params: P) -> Result, QueryError> + where + I: BorrowToSql, + P: IntoIterator, + P::IntoIter: ExactSizeIterator, + { + let stream = self.query_iter(params).await?; + pin_mut!(stream); + + let row = match stream.try_next().await { + Ok(Some(row)) => row, + Ok(None) => return Ok(None), + Err(_) => return Err(QueryError::HasNoRow), + }; + + match stream.try_next().await { + Ok(Some(_)) | Err(_) => return Err(QueryError::HasMoreThanOneRow(row)), + Ok(None) => (), + } + + Ok(Some(row)) + } + + /// Returns the only result of the query. + /// If the query has no result, it will return `Err(HasNoRow)`, but if + /// the query result has more than one row, the first row will be stored + /// in `Err(QueryError::HasMoreThanOneRow)`. + pub async fn query_single(&self, params: P) -> Result> + where + I: BorrowToSql, + P: IntoIterator, + P::IntoIter: ExactSizeIterator, + { + let stream = self.query_iter(params).await?; + pin_mut!(stream); + + let row = match stream.try_next().await { + Ok(Some(row)) => row, + _ => return Err(QueryError::HasNoRow), + }; + + match stream.try_next().await { + Ok(Some(_)) | Err(_) => return Err(QueryError::HasMoreThanOneRow(row)), + Ok(None) => (), + } + + Ok(row) + } + + /// Execute the query and return the amount of row update or `QueryError::ExecuteFailed` + /// in case of error. + pub async fn execute(&self, params: P) -> Result> + where + I: BorrowToSql, + P: IntoIterator, + P::IntoIter: ExactSizeIterator, + { + let statement = self.prepare().await?; + match self.client.execute_raw(&statement, params).await { + Ok(n) => Ok(n as usize), + Err(err) => Err(QueryError::ExecuteFailed(err)), + } + } + + async fn prepare(&self) -> Result> { + self.client + .prepare_typed_cached(self.query.query(), self.query.types()) + .await + .map_err(|_| QueryError::PreparationFailed(self.query.clone())) + } +} diff --git a/crates/taom-database/src/query.rs b/crates/taom-database/src/query.rs new file mode 100644 index 0000000..6a8aeb8 --- /dev/null +++ b/crates/taom-database/src/query.rs @@ -0,0 +1,41 @@ +use std::fmt; +use tokio_postgres::types::Type; + +/// Representation of sql query, +/// store the query string and the types of all argument like a big pointer. +#[derive(Clone)] +pub struct Query<'q>(&'q str, &'q [Type]); + +impl<'q> Query<'q> { + #[inline] + pub const fn params(query: &'q str, types: &'q [Type]) -> Self { + Self(query, types) + } + + #[inline] + pub const fn new(query: &'q str) -> Self { + Self(query, &[]) + } + + pub fn query(&self) -> &'q str { + self.0 + } + + pub(crate) fn types(&self) -> &'q [Type] { + self.1 + } +} + +impl<'q> fmt::Display for Query<'q> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.0, f) + } +} + +impl<'q> fmt::Debug for Query<'q> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self.0, f) + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..626bfd4 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,3 @@ +pub use queries::QUERIES; + +mod queries; diff --git a/src/database/queries.rs b/src/database/queries.rs new file mode 100644 index 0000000..c6bdf1a --- /dev/null +++ b/src/database/queries.rs @@ -0,0 +1,46 @@ +use taom_database::{ConstQueryMap, Query}; +use tokio_postgres::types::Type; + +pub const QUERIES: ConstQueryMap<&str, 9> = ConstQueryMap::new([ + // CONNECTION + ("find-player-info", Query::params( + "SELECT uuid, nickname FROM players WHERE id = $1", + &[Type::INT4] + )), + ("get-player-permissions", Query::params( + "SELECT permission FROM player_permissions WHERE player_id = $1", + &[Type::INT4] + )), + + // SHIPS + ("get-player-ship", Query::params( + "SELECT data FROM player_ships WHERE player_id = $1 AND slot = $2", + &[Type::INT4, Type::INT4] + )), + ("insert-player-ship", Query::params( + "INSERT INTO player_ships(player_id, slot, last_update, data) VALUES($1, $2, NOW(), $3) ON CONFLICT(player_id, slot) DO UPDATE SET last_update = NOW(), data = EXCLUDED.data", + &[Type::INT4, Type::INT4, Type::JSONB] + )), + + // PLAYERS + ("create-player", Query::params( + "INSERT INTO players(uuid, creation_time, nickname) VALUES($1, NOW(), $2) RETURNING id", + &[Type::UUID, Type::VARCHAR] + )), + ("create-token", Query::params( + "INSERT INTO player_tokens(token, player_id) VALUES($1, $2)", + &[Type::VARCHAR, Type::INT4] + )), + ("find-player-info", Query::params( + "SELECT uuid, nickname FROM players WHERE id = $1", + &[Type::INT4] + )), + ("find-token", Query::params( + "SELECT player_id FROM player_tokens WHERE token = $1", + &[Type::VARCHAR] + )), + ("update-player-connection", Query::params( + "UPDATE players SET last_connection_time = NOW() WHERE id = $1", + &[Type::INT4] + )), +]); diff --git a/src/errors/api.rs b/src/errors/api.rs index 2781449..178a869 100644 --- a/src/errors/api.rs +++ b/src/errors/api.rs @@ -153,8 +153,8 @@ impl ResponseError for RouteError { // to delete '$into_type:path' you need to use proc macros and further manipulation of the AST macro_rules! error_from { - (transform $from:path, $into_type:path, |$err_name:ident| $blk:block) => { - impl From<$from> for $into_type { + (transform$(<$T:ident>)? $from:path, $into_type:path, |$err_name:ident| $blk:block) => { + impl<$($T)?> From<$from> for $into_type { fn from($err_name: $from) -> Self { $blk } @@ -196,3 +196,10 @@ error_from! { transform jsonwebtoken::errors::Error, RouteError, |value| { ErrorCode::JWTAccident(value) ) } } + +error_from! { transform taom_database::error::QueryError, RouteError, |value| { + RouteError::ServerError( + ErrorCause::Database, + ErrorCode::External(value.to_string()) + ) +} } diff --git a/src/main.rs b/src/main.rs index 412b915..c86a281 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,8 +4,10 @@ use actix_governor::{Governor, GovernorConfig, GovernorConfigBuilder}; use actix_web::{middleware, web, App, HttpServer}; use cached::TimedCache; use confy::ConfyError; +use deadpool_postgres::{RecyclingMethod, Runtime}; +use taom_database::config::ConfigBuilder; +use taom_database::error::PoolError; use tokio::sync::Mutex; -use tokio_postgres::NoTls; use crate::app_data::AppData; use crate::config::ApiConfig; @@ -15,6 +17,7 @@ use crate::fetcher::Fetcher; mod app_data; mod config; mod data; +mod database; mod deku_helper; mod errors; mod fetcher; @@ -24,26 +27,6 @@ mod routes; const CONFIG_FILE: Cow<'static, str> = Cow::Borrowed("tsom_api_config.toml"); -async fn setup_pg_pool(api_config: &ApiConfig) -> Result { - use deadpool_postgres::{Config, ManagerConfig, RecyclingMethod, Runtime}; - - let mut pg_config = Config::new(); - pg_config.host = Some(api_config.db_host.clone()); - pg_config.password = Some(api_config.db_password.unsecure().to_string()); - pg_config.user = Some(api_config.db_user.clone()); - pg_config.dbname = Some(api_config.db_database.clone()); - pg_config.manager = Some(ManagerConfig { - recycling_method: RecyclingMethod::Fast, - }); - - let pool = pg_config.create_pool(Some(Runtime::Tokio1), NoTls)?; - - // Try to connect to database to test if the database exist - let _ = pool.get().await?; - - Ok(pool) -} - #[actix_web::main] async fn main() -> Result<(), std::io::Error> { std::env::set_var("RUST_LOG", "info,actix_web=info"); @@ -72,19 +55,20 @@ async fn main() -> Result<(), std::io::Error> { let fetcher = Fetcher::from_config(&config).unwrap(); log::info!("Connection to the database"); - let pg_pool = match setup_pg_pool(&config).await { + let pg_pool = ConfigBuilder::default() + .host(config.db_host.as_str()) + .password(config.db_password.unsecure()) + .user(config.db_user.as_str()) + .database(config.db_database.as_str()) + .recycling_method(RecyclingMethod::Fast) + .runtime(Runtime::Tokio1) + .build() + .await; + + let pg_pool = match pg_pool { Ok(pool) => web::Data::new(pool), - Err(err) => { - use deadpool_postgres::{CreatePoolError, PoolError}; - - if err.is::() { - panic!("an error occured during the creation of the pool") - } else if err.is::() { - panic!("failed to connect to database") - } else { - unreachable!() - } - } + Err(PoolError::Creation) => panic!("an error occured during the creation of the pool"), + Err(PoolError::Connection) => panic!("failed to connect to database"), }; let bind_address = format!("{}:{}", config.listen_address, config.listen_port); diff --git a/src/routes/connection.rs b/src/routes/connection.rs index 696b398..25a0175 100644 --- a/src/routes/connection.rs +++ b/src/routes/connection.rs @@ -1,15 +1,16 @@ use actix_web::{post, web, HttpResponse, Responder}; -use deadpool_postgres::tokio_postgres::types::Type; -use futures::{StreamExt, TryStreamExt}; +use futures::future::join; +use futures::TryStreamExt; use jsonwebtoken::{EncodingKey, Header}; use serde::Deserialize; -use tokio_postgres::Row; +use taom_database::dynamic; use uuid::Uuid; use crate::config::ApiConfig; use crate::data::connection_token::{ConnectionToken, PrivateConnectionToken, ServerAddress}; use crate::data::game_data_token::GameDataToken; use crate::data::player_data::PlayerData; +use crate::database::QUERIES; use crate::errors::api::{ErrorCause, ErrorCode, RequestError, RouteError}; use crate::routes::players::validate_player_token; @@ -27,39 +28,33 @@ async fn game_connect( let pg_client = pg_pool.get().await?; let player_id = validate_player_token(&pg_client, ¶ms.token).await?; - // TODO(SirLynix): to do this with only one query - let find_player_info = pg_client - .prepare_typed_cached( - "SELECT uuid, nickname FROM players WHERE id = $1", - &[Type::INT4], - ) - .await?; - - let get_player_permissions = pg_client - .prepare_typed_cached( - "SELECT permission FROM player_permissions WHERE player_id = $1", - &[Type::INT4], - ) - .await?; - - let player_result = pg_client - .query_opt(&find_player_info, &[&player_id]) - .await? - .ok_or(RouteError::InvalidRequest(RequestError::new( - ErrorCode::AuthenticationInvalidToken, - format!("No player has the id '{player_id}'"), - )))?; - - let uuid: Uuid = player_result.try_get(0)?; - let nickname: String = player_result.try_get(1)?; - let permissions: Vec = pg_client - .query_raw(&get_player_permissions, &[&player_id]) - .await? - .map(|row: Result| row.and_then(|row| row.try_get(0))) - .try_collect() - .await?; + let queries = join( + async { + Ok(QUERIES + .prepare::<(Uuid, String)>("find-player-info", &pg_client) + .query_one([dynamic(&player_id)]) + .await? + .ok_or(RouteError::InvalidRequest(RequestError::new( + ErrorCode::AuthenticationInvalidToken, + format!("No player has the id '{player_id}'"), + )))?) + }, + async { + Ok(QUERIES + .prepare::("get-player-permissions", &pg_client) + .query_iter([dynamic(&player_id)]) + .await? + .try_collect::>() + .await?) + }, + ); - let player_data = PlayerData::new(uuid, nickname, permissions); + let (uuid, player_data) = match queries.await { + (Ok((uuid, nickname)), Ok(permissions)) => { + (uuid, PlayerData::new(uuid, nickname, permissions)) + } + (Err(err), _) | (_, Err(err)) => return Err(err), + }; let server_address = ServerAddress::new(config.game_server_address.as_str(), config.game_server_port); @@ -84,10 +79,7 @@ async fn game_connect( server_address, private_token, ) - .map_err(|_| RouteError::ServerError( - ErrorCause::Internal, - ErrorCode::TokenGenerationFailed, - ))?; + .map_err(|_| RouteError::ServerError(ErrorCause::Internal, ErrorCode::TokenGenerationFailed))?; Ok(HttpResponse::Ok().json(token)) } diff --git a/src/routes/game_server.rs b/src/routes/game_server.rs index 6bd2744..76bff35 100644 --- a/src/routes/game_server.rs +++ b/src/routes/game_server.rs @@ -1,9 +1,10 @@ use actix_web::{get, patch, post, web, HttpRequest, HttpResponse, Responder}; use jsonwebtoken::{decode, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; -use tokio_postgres::types::Type; +use taom_database::{dynamic, FromRow}; use crate::data::game_data_token::GameDataToken; +use crate::database::QUERIES; use crate::errors::api::{ErrorCode, RequestError}; use crate::{config::ApiConfig, errors::api::RouteError}; @@ -105,8 +106,9 @@ async fn refresh_access_token( })) } -#[derive(Serialize)] +#[derive(FromRow, Serialize)] struct GetShipResponse { + #[db_row(rename = "data")] ship_data: serde_json::Value, } @@ -120,22 +122,15 @@ async fn player_ship_get( let access_token = validate_token(&req, &config, "access")?; let pg_client = pg_pool.get().await?; - let get_player_ship = pg_client - .prepare_typed_cached( - "SELECT data FROM player_ships WHERE player_id = $1 AND slot = $2", - &[Type::INT4, Type::INT4], - ) - .await?; - let row = pg_client - .query_opt(&get_player_ship, &[&access_token.player_db_id, &*path]) + let row = QUERIES + .prepare::("get-player-ship", &pg_client) + .query_one([dynamic(&access_token.player_db_id), &*path]) .await?; Ok(match row { - Some(row) => HttpResponse::Ok().json(GetShipResponse { - ship_data: row.get(0), - }), - None => HttpResponse::NotFound().finish() + Some(response) => HttpResponse::Ok().json(response), + None => HttpResponse::NotFound().finish(), }) } @@ -155,18 +150,10 @@ async fn player_ship_patch( let access_token = validate_token(&req, &config, "access")?; let pg_client = pg_pool.get().await?; - let insert_player_ship = pg_client - .prepare_typed_cached( - "INSERT INTO player_ships(player_id, slot, last_update, data) VALUES($1, $2, NOW(), $3) ON CONFLICT(player_id, slot) DO UPDATE SET last_update = NOW(), data = EXCLUDED.data", - &[Type::INT4, Type::INT4, Type::JSONB], - ) - .await?; - pg_client - .execute( - &insert_player_ship, - &[&access_token.player_db_id, &*path, ¶ms.data], - ) + QUERIES + .prepare::<()>("insert-player-ship", &pg_client) + .execute([dynamic(&access_token.player_db_id), &*path, ¶ms.data]) .await?; Ok(HttpResponse::Ok().finish()) diff --git a/src/routes/players.rs b/src/routes/players.rs index 7e8e357..27907a3 100644 --- a/src/routes/players.rs +++ b/src/routes/players.rs @@ -1,12 +1,13 @@ use actix_web::{post, web, HttpResponse, Responder}; -use deadpool_postgres::tokio_postgres::types::Type; use rand_core::OsRng; use serde::{Deserialize, Serialize}; +use taom_database::{dynamic, FromRow}; use uuid::Uuid; use crate::config::ApiConfig; use crate::data::token::Token; +use crate::database::QUERIES; use crate::errors::api::ErrorCause; use crate::errors::api::{ErrorCode, RequestError, RouteError}; @@ -58,24 +59,6 @@ async fn create( } } - let uuid = Uuid::new_v4(); - - let mut pg_client = pg_pool.get().await?; - - let create_player_statement = pg_client - .prepare_typed_cached( - "INSERT INTO players(uuid, creation_time, nickname) VALUES($1, NOW(), $2) RETURNING id", - &[Type::UUID, Type::VARCHAR], - ) - .await?; - - let create_token_statement = pg_client - .prepare_typed_cached( - "INSERT INTO player_tokens(token, player_id) VALUES($1, $2)", - &[Type::VARCHAR, Type::INT4], - ) - .await?; - let Ok(token) = Token::generate(OsRng) else { return Err(RouteError::ServerError( ErrorCause::Internal, @@ -83,18 +66,19 @@ async fn create( )); }; - let transaction = pg_client.transaction().await?; - let created_player_result = transaction - .query_one(&create_player_statement, &[&uuid, &nickname]) - .await?; + let uuid = Uuid::new_v4(); - let player_id: i32 = created_player_result.try_get(0)?; + let pg_client = pg_pool.get().await?; - transaction - .execute(&create_token_statement, &[&token, &player_id]) + let player_id = QUERIES + .prepare::("create-player", &pg_client) + .query_single([dynamic(&uuid), &nickname]) .await?; - transaction.commit().await?; + QUERIES + .prepare::<()>("create-token", &pg_client) + .execute([dynamic(&token), &player_id]) + .await?; Ok(HttpResponse::Ok().json(CreatePlayerResponse { uuid, token })) } @@ -104,7 +88,7 @@ struct AuthenticationParams { token: String, } -#[derive(Serialize)] +#[derive(FromRow, Serialize)] struct AuthenticationResponse { uuid: Uuid, nickname: String, @@ -118,15 +102,9 @@ async fn auth( let pg_client = pg_pool.get().await?; let player_id = validate_player_token(&pg_client, ¶ms.token).await?; - let find_player_info = pg_client - .prepare_typed_cached( - "SELECT uuid, nickname FROM players WHERE id = $1", - &[Type::INT4], - ) - .await?; - - let player_result = pg_client - .query_opt(&find_player_info, &[&player_id]) + let auth_response = QUERIES + .prepare::("find-play-info", &pg_client) + .query_one([dynamic(&player_id)]) .await? .ok_or(RouteError::InvalidRequest(RequestError::new( ErrorCode::AuthenticationInvalidToken, @@ -136,10 +114,7 @@ async fn auth( // Update last connection time in a separate task as its result won't affect the route tokio::spawn(async move { update_player_connection(&pg_client, player_id).await }); - Ok(HttpResponse::Ok().json(AuthenticationResponse { - uuid: player_result.try_get(0)?, - nickname: player_result.try_get(1)?, - })) + Ok(HttpResponse::Ok().json(auth_response)) } pub async fn validate_player_token( @@ -160,39 +135,24 @@ pub async fn validate_player_token( ))); } - let find_token_statement = pg_client - .prepare_typed_cached( - "SELECT player_id FROM player_tokens WHERE token = $1", - &[Type::VARCHAR], - ) - .await?; - - let token_result = pg_client - .query_opt(&find_token_statement, &[&token]) + let player_id = QUERIES + .prepare::("find-token", pg_client) + .query_one([dynamic(&token)]) .await? .ok_or(RouteError::InvalidRequest(RequestError::new( ErrorCode::AuthenticationInvalidToken, format!("No player has the token '{token}'"), )))?; - Ok(token_result.try_get(0)?) + Ok(player_id) } async fn update_player_connection(pg_client: &deadpool_postgres::Client, player_id: i32) { - match pg_client - .prepare_typed_cached( - "UPDATE players SET last_connection_time = NOW() WHERE id = $1", - &[Type::INT4], - ) + if let Err(err) = QUERIES + .prepare::<()>("update-player-connection", pg_client) + .execute([dynamic(&player_id)]) .await { - Ok(statement) => { - if let Err(err) = pg_client.execute(&statement, &[&player_id]).await { - log::error!("Failed to update player {player_id} connection time: {err}"); - } - } - Err(err) => { - log::error!("Failed to update player {player_id} connection time (failed to prepare query): {err}"); - } + log::error!("Failed to update player {player_id} connection time: {err}"); } }