test with kerkour architecture (https://kerkour.com/rust-web-application-clean-architecture)
This commit is contained in:
53
src/config.rs
Normal file
53
src/config.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use anyhow::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub http: Http,
|
||||
pub database: Database,
|
||||
pub graphql: Grapthql,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Http {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Database {
|
||||
pub url: String,
|
||||
pub pool_size: u32,
|
||||
pub max_lifetime: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Grapthql {
|
||||
pub schema_location: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Result<Self, Error> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
let http = envy::prefixed("HTTP_")
|
||||
.from_env::<Http>()
|
||||
.expect("Failed to load http config");
|
||||
|
||||
let database = envy::prefixed("DATABASE_")
|
||||
.from_env::<Database>()
|
||||
.expect("Failed to load database config");
|
||||
|
||||
let graphql = envy::prefixed("GRAPHQL_")
|
||||
.from_env::<Grapthql>()
|
||||
.expect("Failed to load graphql config");
|
||||
|
||||
let config = Self {
|
||||
http,
|
||||
database,
|
||||
graphql,
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
23
src/database.rs
Normal file
23
src/database.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use sqlx::{self, postgres::PgPoolOptions, Executor, Pool, Postgres};
|
||||
|
||||
use crate::config;
|
||||
|
||||
pub type DB = Pool<Postgres>;
|
||||
pub trait Queryer<'c>: Executor<'c, Database = sqlx::Postgres> {}
|
||||
|
||||
impl<'c> Queryer<'c> for &Pool<Postgres> {}
|
||||
|
||||
pub async fn connect(database: &config::Database) -> Result<DB, Error> {
|
||||
PgPoolOptions::new()
|
||||
.max_connections(database.pool_size)
|
||||
.max_lifetime(Duration::from_secs(database.max_lifetime))
|
||||
.connect(&database.url)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!("{}", err);
|
||||
err.into()
|
||||
})
|
||||
}
|
||||
1
src/domain.rs
Normal file
1
src/domain.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod rolle;
|
||||
5
src/domain/rolle.rs
Normal file
5
src/domain/rolle.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod entity;
|
||||
pub mod model;
|
||||
pub mod repository;
|
||||
pub mod service;
|
||||
|
||||
2
src/domain/rolle/entity.rs
Normal file
2
src/domain/rolle/entity.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod rolle;
|
||||
pub use rolle::Rolle;
|
||||
9
src/domain/rolle/entity/rolle.rs
Normal file
9
src/domain/rolle/entity/rolle.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use crate::scalar::{Id, Time};
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
pub struct Rolle {
|
||||
pub id: Id,
|
||||
pub created_at: Time,
|
||||
pub updated_at: Time,
|
||||
pub name: String,
|
||||
}
|
||||
5
src/domain/rolle/model.rs
Normal file
5
src/domain/rolle/model.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod create_rolle_input;
|
||||
mod rolle;
|
||||
mod update_rolle_input;
|
||||
|
||||
pub use create_rolle_input::CreateRolleInput;
|
||||
7
src/domain/rolle/model/create_rolle_input.rs
Normal file
7
src/domain/rolle/model/create_rolle_input.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use async_graphql::InputObject;
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct CreateRolleInput {
|
||||
/// Der Name einer Rolle
|
||||
pub name: String,
|
||||
}
|
||||
15
src/domain/rolle/model/rolle.rs
Normal file
15
src/domain/rolle/model/rolle.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use async_graphql::SimpleObject;
|
||||
|
||||
use crate::scalar::{Id, Time};
|
||||
|
||||
#[derive(Debug, SimpleObject)]
|
||||
pub struct Rolle {
|
||||
/// Die ID einer Rolle
|
||||
pub id: Id,
|
||||
|
||||
/// Wann die Rolle erstellt wurde
|
||||
pub created_at: Time,
|
||||
|
||||
/// Der Name einer Rolle
|
||||
pub name: String,
|
||||
}
|
||||
12
src/domain/rolle/model/update_rolle_input.rs
Normal file
12
src/domain/rolle/model/update_rolle_input.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use async_graphql::InputObject;
|
||||
|
||||
use crate::scalar::Id;
|
||||
|
||||
#[derive(InputObject)]
|
||||
pub struct UpdateRolleInput {
|
||||
/// Die ID einer Rolle
|
||||
pub id: Id,
|
||||
|
||||
/// Der Name einer Rolle
|
||||
pub name: String,
|
||||
}
|
||||
20
src/domain/rolle/repository.rs
Normal file
20
src/domain/rolle/repository.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
mod create_rolle;
|
||||
mod delete_rolle;
|
||||
mod find_all_rolle;
|
||||
mod find_rolle;
|
||||
mod update_rolle;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Repository {}
|
||||
|
||||
impl Repository {
|
||||
pub fn new() -> Repository {
|
||||
Repository {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Repository {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
30
src/domain/rolle/repository/create_rolle.rs
Normal file
30
src/domain/rolle/repository/create_rolle.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use anyhow::Error;
|
||||
|
||||
use super::Repository;
|
||||
use crate::{database::Queryer, domain::rolle::entity};
|
||||
|
||||
impl Repository {
|
||||
pub async fn create_rolle<'c, C: Queryer<'c>>(
|
||||
&self,
|
||||
db: C,
|
||||
rolle: &entity::Rolle,
|
||||
) -> Result<entity::Rolle, Error> {
|
||||
const QUERY: &str = "insert into rolle (id, created_at, updated_at,
|
||||
name) values ($1, $2, $3, $4) returning *";
|
||||
|
||||
match sqlx::query_as::<_, entity::Rolle>(QUERY)
|
||||
.bind(rolle.id)
|
||||
.bind(rolle.created_at)
|
||||
.bind(rolle.updated_at)
|
||||
.bind(&rolle.name)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
tracing::error!("{}", &err);
|
||||
Err(err.into())
|
||||
}
|
||||
Ok(user) => Ok(user),
|
||||
}
|
||||
}
|
||||
}
|
||||
0
src/domain/rolle/repository/delete_rolle.rs
Normal file
0
src/domain/rolle/repository/delete_rolle.rs
Normal file
0
src/domain/rolle/repository/find_all_rolle.rs
Normal file
0
src/domain/rolle/repository/find_all_rolle.rs
Normal file
0
src/domain/rolle/repository/find_rolle.rs
Normal file
0
src/domain/rolle/repository/find_rolle.rs
Normal file
0
src/domain/rolle/repository/update_rolle.rs
Normal file
0
src/domain/rolle/repository/update_rolle.rs
Normal file
17
src/domain/rolle/service.rs
Normal file
17
src/domain/rolle/service.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
mod create_user;
|
||||
|
||||
use super::repository::Repository;
|
||||
use crate::database::DB;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Service {
|
||||
repo: Repository,
|
||||
pub db: DB,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn new(db: DB) -> Self {
|
||||
let repo = Repository::new();
|
||||
Self { repo, db }
|
||||
}
|
||||
}
|
||||
26
src/domain/rolle/service/create_user.rs
Normal file
26
src/domain/rolle/service/create_user.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use anyhow::Error;
|
||||
use chrono::Utc;
|
||||
use ulid::Ulid;
|
||||
|
||||
use super::Service;
|
||||
use crate::domain::rolle::entity;
|
||||
use crate::domain::rolle::model::CreateRolleInput;
|
||||
|
||||
impl Service {
|
||||
pub async fn create_rolle(&self, input: CreateRolleInput) -> Result<entity::Rolle, Error> {
|
||||
// let username_exists = self.check_username_exists(&self.db, &input.name).await?;
|
||||
// if username_exists {
|
||||
// return Err(Error::UsernameAlreadyExists.into());
|
||||
// }
|
||||
|
||||
let rolle_input = entity::Rolle {
|
||||
id: Ulid::new().into(),
|
||||
name: input.name,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
let rolle = self.repo.create_rolle(&self.db, &rolle_input).await?;
|
||||
Ok(rolle)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use axum::{
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use config::Config;
|
||||
use dotenv::dotenv;
|
||||
use mutations::Mutation;
|
||||
use queries::Query;
|
||||
@@ -15,9 +16,13 @@ use sqlx::postgres::PgPool;
|
||||
use std::env;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
mod config;
|
||||
mod database;
|
||||
mod domain;
|
||||
mod models;
|
||||
mod mutations;
|
||||
mod queries;
|
||||
mod scalar;
|
||||
|
||||
async fn ping() -> String {
|
||||
format!(
|
||||
@@ -36,6 +41,8 @@ async fn main() -> Result<()> {
|
||||
dotenv().ok();
|
||||
env_logger::init();
|
||||
|
||||
Config::load();
|
||||
|
||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set");
|
||||
let db_pool = PgPool::connect(&database_url).await?;
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ use sqlx::{FromRow, PgPool, Type};
|
||||
|
||||
#[derive(SimpleObject, Debug, FromRow, Deserialize, Serialize, Type)]
|
||||
pub struct Typ {
|
||||
/// Die ID eines Geräte-Typs
|
||||
pub id: i32,
|
||||
|
||||
/// Name eines Typs
|
||||
/// Der Name eines Geräte-Typs
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
|
||||
10
src/scalar.rs
Normal file
10
src/scalar.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
/// It is easier to track each type alias if this file is located on the top-level directory (here)
|
||||
/// than in each domain. Also, separating them will create a lot of duplicate code.
|
||||
use uuid::Uuid;
|
||||
|
||||
pub type Time = chrono::DateTime<chrono::Utc>;
|
||||
|
||||
/// The ID scalar type represents a unique identifier, often used to refetch an object or as key for a cache.
|
||||
/// The ID type appears in a JSON response as a String; however, it is not intended to be human-readable.
|
||||
/// When expected as an input type, any string (such as "4") or integer (such as 4) input value will be accepted as an ID.
|
||||
pub type Id = Uuid;
|
||||
Reference in New Issue
Block a user