ad ded migration
This commit is contained in:
parent
19eff26934
commit
6d08c39a6d
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
|
/migration/target
|
||||||
|
|
||||||
# Added by cargo
|
# Added by cargo
|
||||||
#
|
#
|
||||||
|
3705
Cargo.lock
generated
Normal file
3705
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ anyhow = "1.0.95"
|
|||||||
async-trait = "0.1.86"
|
async-trait = "0.1.86"
|
||||||
axum = { version = "0.8.1", features = ["json"] }
|
axum = { version = "0.8.1", features = ["json"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
sea-orm = { version = "1.1.4", features = ["runtime-tokio-native-tls", "sqlx-postgres"] }
|
sea-orm = { version = "1.1.4", features = ["runtime-tokio-native-tls", "sqlx-postgres", "sqlx-sqlite"] }
|
||||||
serde = { version = "1.0.217", features = ["derive"] }
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
serde_json = "1.0.138"
|
serde_json = "1.0.138"
|
||||||
tokio = { version = "1.43.0", features = ["full"] }
|
tokio = { version = "1.43.0", features = ["full"] }
|
||||||
|
2400
migration/Cargo.lock
generated
Normal file
2400
migration/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
migration/Cargo.toml
Normal file
22
migration/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "migration"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "migration"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||||
|
|
||||||
|
[dependencies.sea-orm-migration]
|
||||||
|
version = "1.1.0"
|
||||||
|
features = [
|
||||||
|
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
|
||||||
|
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
|
||||||
|
# e.g.
|
||||||
|
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
|
||||||
|
"sqlx-sqlite", # `DATABASE_DRIVER` feature
|
||||||
|
]
|
41
migration/README.md
Normal file
41
migration/README.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Running Migrator CLI
|
||||||
|
|
||||||
|
- Generate a new migration file
|
||||||
|
```sh
|
||||||
|
cargo run -- generate MIGRATION_NAME
|
||||||
|
```
|
||||||
|
- Apply all pending migrations
|
||||||
|
```sh
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
```sh
|
||||||
|
cargo run -- up
|
||||||
|
```
|
||||||
|
- Apply first 10 pending migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- up -n 10
|
||||||
|
```
|
||||||
|
- Rollback last applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- down
|
||||||
|
```
|
||||||
|
- Rollback last 10 applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- down -n 10
|
||||||
|
```
|
||||||
|
- Drop all tables from the database, then reapply all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- fresh
|
||||||
|
```
|
||||||
|
- Rollback all applied migrations, then reapply all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- refresh
|
||||||
|
```
|
||||||
|
- Rollback all applied migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- reset
|
||||||
|
```
|
||||||
|
- Check the status of all migrations
|
||||||
|
```sh
|
||||||
|
cargo run -- status
|
||||||
|
```
|
14
migration/src/lib.rs
Normal file
14
migration/src/lib.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
pub use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
mod m20220101_000001_create_table;
|
||||||
|
|
||||||
|
pub struct Migrator;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigratorTrait for Migrator {
|
||||||
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
|
vec![
|
||||||
|
Box::new(m20220101_000001_create_table::Migration),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
41
migration/src/m20220101_000001_create_table.rs
Normal file
41
migration/src/m20220101_000001_create_table.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
// Replace the sample below with your own migration scripts
|
||||||
|
todo!();
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table(Post::Table)
|
||||||
|
.if_not_exists()
|
||||||
|
.col(pk_auto(Post::Id))
|
||||||
|
.col(string(Post::Title))
|
||||||
|
.col(string(Post::Text))
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
// Replace the sample below with your own migration scripts
|
||||||
|
todo!();
|
||||||
|
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table(Post::Table).to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(DeriveIden)]
|
||||||
|
enum Post {
|
||||||
|
Table,
|
||||||
|
Id,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
}
|
6
migration/src/main.rs
Normal file
6
migration/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[async_std::main]
|
||||||
|
async fn main() {
|
||||||
|
cli::run_cli(migration::Migrator).await;
|
||||||
|
}
|
34
src/base/controller.rs
Normal file
34
src/base/controller.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use crate::{base::service::BaseService, utils::errors::AppError};
|
||||||
|
use axum::{http::StatusCode, response::IntoResponse, Json};
|
||||||
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait};
|
||||||
|
|
||||||
|
pub struct BaseController<Entity, Model, ActiveModel>
|
||||||
|
where
|
||||||
|
Entity: EntityTrait,
|
||||||
|
Model: ModelTrait + Send + Sync + 'static,
|
||||||
|
ActiveModel: ActiveModelTrait + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
service: BaseService<Entity, Model, ActiveModel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Entity, Model, ActiveModel> BaseController<Entity, Model, ActiveModel>
|
||||||
|
where
|
||||||
|
Entity: EntityTrait,
|
||||||
|
Model: ModelTrait + Send + Sync + 'static,
|
||||||
|
ActiveModel: ActiveModelTrait + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
service: BaseService::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
}
|
10
src/base/entity.rs
Normal file
10
src/base/entity.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
use sea_orm::entity::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,
|
||||||
|
}
|
22
src/base/i_service.rs
Normal file
22
src/base/i_service.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use crate::utils::errors::AppError;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait IService<Entity, Model, ActiveModel>
|
||||||
|
where
|
||||||
|
Entity: EntityTrait,
|
||||||
|
Model: ModelTrait + Send + Sync,
|
||||||
|
ActiveModel: ActiveModelTrait + Send + Sync,
|
||||||
|
{
|
||||||
|
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>;
|
||||||
|
}
|
4
src/base/mod.rs
Normal file
4
src/base/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pub mod controller;
|
||||||
|
pub mod entity;
|
||||||
|
pub mod i_service;
|
||||||
|
pub mod service;
|
84
src/base/service.rs
Normal file
84
src/base/service.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct BaseService<Entity, Model, ActiveModel>
|
||||||
|
where
|
||||||
|
Entity: EntityTrait,
|
||||||
|
Model: ModelTrait + Send + Sync + 'static,
|
||||||
|
ActiveModel: ActiveModelTrait + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
entity: std::marker::PhantomData<Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Entity, Model, ActiveModel> BaseService<Entity, Model, ActiveModel>
|
||||||
|
where
|
||||||
|
Entity: EntityTrait,
|
||||||
|
Model: ModelTrait + Send + Sync + 'static,
|
||||||
|
ActiveModel: ActiveModelTrait + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
entity: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<Entity, Model, ActiveModel> IService<Entity, Model, ActiveModel>
|
||||||
|
for BaseService<Entity, Model, ActiveModel>
|
||||||
|
where
|
||||||
|
Entity: EntityTrait,
|
||||||
|
Model: ModelTrait + Send + Sync + 'static,
|
||||||
|
ActiveModel: ActiveModelTrait + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
async fn create(&self, db: &DatabaseConnection, data: ActiveModel) -> Result<Model, AppError> {
|
||||||
|
Ok(data.insert(db).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
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?)
|
||||||
|
}
|
||||||
|
}
|
61
src/routes/product/entity.rs
Normal file
61
src/routes/product/entity.rs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
73
src/utils/errors.rs
Normal file
73
src/utils/errors.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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
src/utils/mod.rs
Normal file
1
src/utils/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod errors;
|
Loading…
Reference in New Issue
Block a user