99% of base complete
This commit is contained in:
@@ -1,34 +1,33 @@
|
||||
use crate::{base::service::BaseService, utils::errors::AppError};
|
||||
use anyhow::Result;
|
||||
use axum::{http::StatusCode, response::IntoResponse, Json};
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait};
|
||||
use diesel::Identifiable;
|
||||
use serde::Serialize;
|
||||
use std::marker::PhantomData;
|
||||
use crate::base::{entity::BaseEntity, i_service::IBaseService};
|
||||
|
||||
pub struct BaseController<Entity, Model, ActiveModel>
|
||||
pub struct BaseController<T, S>
|
||||
where
|
||||
Entity: EntityTrait,
|
||||
Model: ModelTrait + Send + Sync + 'static,
|
||||
ActiveModel: ActiveModelTrait + Send + Sync + 'static,
|
||||
T: BaseEntity + Identifiable<Id = i32> + Serialize + Send + Sync + 'static,
|
||||
S: IBaseService<T> + Send + Sync + 'static,
|
||||
{
|
||||
service: BaseService<Entity, Model, ActiveModel>,
|
||||
service: S,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<Entity, Model, ActiveModel> BaseController<Entity, Model, ActiveModel>
|
||||
impl<T, S> BaseController<T, S>
|
||||
where
|
||||
Entity: EntityTrait,
|
||||
Model: ModelTrait + Send + Sync + 'static,
|
||||
ActiveModel: ActiveModelTrait + Send + Sync + 'static,
|
||||
T: BaseEntity + Identifiable<Id = i32> + Serialize + Send + Sync + 'static,
|
||||
S: IBaseService<T> + Send + Sync + 'static,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
pub fn new(service: S) -> Self {
|
||||
Self {
|
||||
service: BaseService::new(),
|
||||
service,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
&self,
|
||||
state: State<DatabaseConnection>,
|
||||
Json(data): Json<ActiveModel>,
|
||||
) -> Result<IntoResponse, AppError> {
|
||||
let model = self.service.create(&state, data).await?;
|
||||
Ok((StatusCode::CREATED, Json(model)))
|
||||
pub async fn get_all(&self) -> Result<impl IntoResponse> {
|
||||
let entities = self.service.get_all().await?;
|
||||
Ok((StatusCode::OK, Json(entities)))
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,17 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Serialize, Deserialize)]
|
||||
pub struct BaseEntity {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
pub trait BaseEntity {
|
||||
fn id(&self) -> i32;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Entity {
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
impl BaseEntity for Entity {
|
||||
fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
|
@@ -1,22 +1,11 @@
|
||||
use crate::utils::errors::AppError;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait};
|
||||
use diesel::Identifiable;
|
||||
|
||||
#[async_trait]
|
||||
pub trait IService<Entity, Model, ActiveModel>
|
||||
where
|
||||
Entity: EntityTrait,
|
||||
Model: ModelTrait + Send + Sync,
|
||||
ActiveModel: ActiveModelTrait + Send + Sync,
|
||||
pub trait IBaseService<T>: Send + Sync
|
||||
where
|
||||
T: Identifiable<Id = i32> + Send + Sync + 'static
|
||||
{
|
||||
async fn create(&self, db: &DatabaseConnection, data: ActiveModel) -> Result<Model, AppError>;
|
||||
async fn get_by_id(&self, db: &DatabaseConnection, id: i32) -> Result<Option<Model>, AppError>;
|
||||
async fn get_all(&self, db: &DatabaseConnection) -> Result<Vec<Model>, AppError>;
|
||||
async fn update(
|
||||
&self,
|
||||
db: &DatabaseConnection,
|
||||
id: i32,
|
||||
data: ActiveModel,
|
||||
) -> Result<Model, AppError>;
|
||||
async fn delete(&self, db: &DatabaseConnection, id: i32) -> Result<(), AppError>;
|
||||
async fn get_all(&self) -> Result<Vec<T>>;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
pub mod controller;
|
||||
pub mod entity;
|
||||
pub mod i_service;
|
||||
pub mod service;
|
||||
pub mod controller;
|
||||
pub mod entity;
|
||||
|
@@ -1,84 +1,61 @@
|
||||
use crate::base::i_service::IService;
|
||||
use crate::utils::errors::AppError;
|
||||
use async_trait::async_trait;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, Order, QueryFilter,
|
||||
QueryOrder, Set,
|
||||
};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub struct BaseService<Entity, Model, ActiveModel>
|
||||
use anyhow::Error;
|
||||
use async_trait::async_trait;
|
||||
use diesel::prelude::*;
|
||||
use diesel::associations::HasTable;
|
||||
use diesel::query_dsl::methods::LoadQuery;
|
||||
|
||||
use crate::config::db::DbPool;
|
||||
use super::i_service::IBaseService;
|
||||
|
||||
pub struct BaseService<T, U>
|
||||
where
|
||||
Entity: EntityTrait,
|
||||
Model: ModelTrait + Send + Sync + 'static,
|
||||
ActiveModel: ActiveModelTrait + Send + Sync + 'static,
|
||||
T: Identifiable<Id = i32> + Queryable<U::SqlType, diesel::pg::Pg> + Send + Sync + 'static,
|
||||
U: Table + HasTable<Table = U> + LoadQuery<'static, PgConnection, T> + Send + Sync + 'static,
|
||||
{
|
||||
entity: std::marker::PhantomData<Entity>,
|
||||
pool: DbPool,
|
||||
_marker: PhantomData<(T, U)>,
|
||||
}
|
||||
|
||||
impl<Entity, Model, ActiveModel> BaseService<Entity, Model, ActiveModel>
|
||||
impl<T, U> BaseService<T, U>
|
||||
where
|
||||
Entity: EntityTrait,
|
||||
Model: ModelTrait + Send + Sync + 'static,
|
||||
ActiveModel: ActiveModelTrait + Send + Sync + 'static,
|
||||
T: Identifiable<Id = i32> + Queryable<U::SqlType, diesel::pg::Pg> + Send + Sync + 'static,
|
||||
U: Table
|
||||
+ HasTable<Table = U>
|
||||
+ LoadQuery<'static, PgConnection, T>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
pub fn new(pool: DbPool) -> Self {
|
||||
Self {
|
||||
entity: std::marker::PhantomData,
|
||||
pool,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<Entity, Model, ActiveModel> IService<Entity, Model, ActiveModel>
|
||||
for BaseService<Entity, Model, ActiveModel>
|
||||
impl<T, U> IBaseService<T> for BaseService<T, U>
|
||||
where
|
||||
Entity: EntityTrait,
|
||||
Model: ModelTrait + Send + Sync + 'static,
|
||||
ActiveModel: ActiveModelTrait + Send + Sync + 'static,
|
||||
T: Identifiable<Id = i32> + Queryable<U::SqlType, diesel::pg::Pg> + Send + Sync + 'static,
|
||||
U: Table
|
||||
+ HasTable<Table = U>
|
||||
+ LoadQuery<'static, PgConnection, T>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
{
|
||||
async fn create(&self, db: &DatabaseConnection, data: ActiveModel) -> Result<Model, AppError> {
|
||||
Ok(data.insert(db).await?)
|
||||
async fn get_all(&self) -> Result<Vec<T>, Error> {
|
||||
let pool = self.pool.clone();
|
||||
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let mut conn = pool.get().expect("Failed to get DB connection");
|
||||
U::table().load::<T>(&mut conn)
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_by_id(&self, db: &DatabaseConnection, id: i32) -> Result<Option<Model>, AppError> {
|
||||
Ok(Entity::find_by_id(id).one(db).await?)
|
||||
}
|
||||
|
||||
async fn get_all(&self, db: &DatabaseConnection) -> Result<Vec<Model>, AppError> {
|
||||
Ok(Entity::find().all(db).await?)
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&self,
|
||||
db: &DatabaseConnection,
|
||||
id: i32,
|
||||
data: ActiveModel,
|
||||
) -> Result<Model, AppError> {
|
||||
let model = Entity::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound)?;
|
||||
let mut active_model: ActiveModel = model.into();
|
||||
|
||||
// Update fields individually, handling Optionals correctly:
|
||||
// Example: Assume your Model has a 'name' field.
|
||||
if let Some(name) = data.get_name() {
|
||||
// Assuming ActiveModel has get_name()
|
||||
active_model.set_name(name); // And set_name()
|
||||
}
|
||||
if let Some(description) = data.get_description() {
|
||||
active_model.set_description(description);
|
||||
}
|
||||
// ... update other fields similarly ...
|
||||
|
||||
Ok(active_model.update(db).await?)
|
||||
}
|
||||
|
||||
async fn delete(&self, db: &DatabaseConnection, id: i32) -> Result<(), AppError> {
|
||||
let model = Entity::find_by_id(id)
|
||||
.one(db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound)?;
|
||||
Ok(model.delete(db).await?)
|
||||
}
|
||||
}
|
||||
}
|
23
src/config/db.rs
Normal file
23
src/config/db.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::r2d2::{self, ConnectionManager, Pool, PoolError};
|
||||
|
||||
use std::env;
|
||||
|
||||
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
|
||||
|
||||
pub fn establish_connection_pool() -> Result<DbPool, PoolError> {
|
||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
|
||||
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||
Pool::builder().build(manager)
|
||||
}
|
||||
|
||||
pub fn get_connection(
|
||||
pool: &DbPool,
|
||||
) -> Result<r2d2::PooledConnection<ConnectionManager<PgConnection>>, PoolError> {
|
||||
pool.get()
|
||||
}
|
||||
|
||||
pub fn init_db() -> DbPool {
|
||||
establish_connection_pool().expect("Failed to create connection pool")
|
||||
}
|
1
src/config/mod.rs
Normal file
1
src/config/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod db;
|
24
src/main.rs
24
src/main.rs
@@ -1,12 +1,26 @@
|
||||
use dotenvy::dotenv;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{serve, Router};
|
||||
use config::db::init_db;
|
||||
use tokio::net::TcpListener;
|
||||
use crate::routes::user::controller::UserController;
|
||||
use crate::routes::user::service::UserService;
|
||||
|
||||
mod base;
|
||||
mod config;
|
||||
mod models;
|
||||
mod routes;
|
||||
mod utils;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn , std::error::Error>> {
|
||||
//
|
||||
async fn main() {
|
||||
let pool = init_db();
|
||||
|
||||
let user_service = UserService::new(pool);
|
||||
let user_controller = Arc::new(UserController::new(user_service));
|
||||
|
||||
let app = Router::new()
|
||||
.merge(UserController::routes())
|
||||
.with_state(user_controller);
|
||||
|
||||
let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
1
src/routes/mod.rs
Normal file
1
src/routes/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod user;
|
@@ -1,61 +0,0 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::base::entity::BaseEntity;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "product")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
// ... other product-specific fields ...
|
||||
pub created_at: DateTime,
|
||||
pub updated_at: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
// ActiveModel - CORRECT STRUCTURE (No DeriveModel!)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ActiveModel {
|
||||
pub id: sea_orm::ActiveValue<i32>,
|
||||
pub name: sea_orm::ActiveValue<String>,
|
||||
pub description: sea_orm::ActiveValue<Option<String>>,
|
||||
// ... other fields ...
|
||||
pub created_at: sea_orm::ActiveValue<DateTime>,
|
||||
pub updated_at: sea_orm::ActiveValue<DateTime>,
|
||||
}
|
||||
|
||||
impl Entity {
|
||||
pub fn find_by_id(id: i32) -> Select<'static> {
|
||||
Self::find().filter(Column::Id.eq(id))
|
||||
}
|
||||
}
|
||||
|
||||
// Implement get and set methods for the ActiveModel
|
||||
impl ActiveModel {
|
||||
pub fn get_name(&self) -> Option<String> {
|
||||
match self.name {
|
||||
sea_orm::ActiveValue::Set(val) => Some(val),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
pub fn set_name(&mut self, val: String) {
|
||||
self.name = sea_orm::Set(val);
|
||||
}
|
||||
|
||||
pub fn get_description(&self) -> Option<String> {
|
||||
match self.description {
|
||||
sea_orm::ActiveValue::Set(val) => Some(val),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_description(&mut self, val: String) {
|
||||
self.description = sea_orm::Set(val);
|
||||
}
|
||||
}
|
29
src/routes/user/controller.rs
Normal file
29
src/routes/user/controller.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use axum::Router;
|
||||
use axum::routing::get;
|
||||
use crate::base::controller::BaseController;
|
||||
|
||||
use super::model::User;
|
||||
use super::service::UserService;
|
||||
|
||||
pub struct UserController {
|
||||
base_controller: BaseController<User, UserService>,
|
||||
}
|
||||
|
||||
impl UserController {
|
||||
pub fn new(service: UserService) -> Self {
|
||||
Self {
|
||||
base_controller: BaseController::new(service),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Router {
|
||||
Router::new()
|
||||
.route("/users", get(Self::get_users))
|
||||
}
|
||||
|
||||
async fn get_users(
|
||||
State(controller): State<Arc<UserController>>,
|
||||
) -> impl IntoResponse {
|
||||
controller.base_controller.get_all().await
|
||||
}
|
||||
}
|
4
src/routes/user/mod.rs
Normal file
4
src/routes/user/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod model;
|
||||
pub mod schema;
|
||||
pub mod service;
|
||||
pub mod controller;
|
18
src/routes/user/model.rs
Normal file
18
src/routes/user/model.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::base::entity::BaseEntity;
|
||||
use super::schema::users;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Identifiable)]
|
||||
#[diesel(table_name = users)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl BaseEntity for User {
|
||||
fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
}
|
7
src/routes/user/schema.rs
Normal file
7
src/routes/user/schema.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
diesel::table! {
|
||||
users (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
email -> Varchar,
|
||||
}
|
||||
}
|
5
src/routes/user/service.rs
Normal file
5
src/routes/user/service.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use crate::base::service::BaseService;
|
||||
use super::model::User;
|
||||
use super::schema::users;
|
||||
|
||||
pub type UserService = BaseService<User, users::table>;
|
9
src/schema.rs
Normal file
9
src/schema.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
users (id) {
|
||||
id -> Int4,
|
||||
name -> Varchar,
|
||||
email -> Varchar,
|
||||
}
|
||||
}
|
@@ -1,73 +0,0 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use sea_orm::error::DbErr;
|
||||
use serde_json::json;
|
||||
use std::error::Error as StdError;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
#[error("Not Found")]
|
||||
NotFound,
|
||||
#[error("Bad Request: {0}")]
|
||||
BadRequest(String),
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
#[error("Internal Server Error: {0}")]
|
||||
InternalServerError(String),
|
||||
#[error("Database Error: {0}")]
|
||||
DatabaseError(#[from] DbErr),
|
||||
#[error("Other Error: {0}")]
|
||||
Other(#[from] anyhow::Error), // For other errors using anyhow
|
||||
}
|
||||
|
||||
// Implement the IntoResponse trait for AppError
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match self {
|
||||
AppError::NotFound => (StatusCode::NOT_FOUND, "Not Found".to_string()),
|
||||
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
|
||||
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".to_string()),
|
||||
AppError::InternalServerError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
|
||||
AppError::DatabaseError(err) => {
|
||||
// Log the database error for debugging (important!)
|
||||
eprintln!("Database Error: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Database error occurred".to_string(),
|
||||
)
|
||||
}
|
||||
AppError::Other(err) => {
|
||||
eprintln!("Other Error: {}", err);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"An unexpected error occurred".to_string(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let body = Json(json!({
|
||||
"error": message,
|
||||
}));
|
||||
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
// You can create a From trait implementation if you have a custom error type
|
||||
// and want to convert it into AppError. For example, if you had a
|
||||
// `MyCustomError`:
|
||||
|
||||
// impl From<MyCustomError> for AppError {
|
||||
// fn from(err: MyCustomError) -> Self {
|
||||
// AppError::InternalServerError(err.to_string()) // Or map to a more specific AppError
|
||||
// }
|
||||
// }
|
||||
|
||||
// Example usage in your service:
|
||||
// async fn my_service_function(...) -> Result<..., AppError> {
|
||||
// let result = some_fallible_function().map_err(|e| AppError::BadRequest(e.to_string()))?;
|
||||
// // ...
|
||||
// }
|
@@ -1 +0,0 @@
|
||||
pub mod errors;
|
Reference in New Issue
Block a user