Cont. feat: expose database connection to ActiveModelBehaviour's methods (#1328)

* feat: expose database connection to `ActiveModelBehaviour`'s methods (#1145)

* Make ActiveModelTrait async

* Add tests

* refactoring

Co-authored-by: teenjuna <53595243+teenjuna@users.noreply.github.com>
This commit is contained in:
Billy Chan 2023-01-12 17:22:46 +08:00 committed by GitHub
parent a9ec15ea02
commit e2b796b093
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 187 additions and 21 deletions

View File

@ -291,11 +291,11 @@ pub trait ActiveModelTrait: Clone + Debug {
Self: ActiveModelBehavior + 'a, Self: ActiveModelBehavior + 'a,
C: ConnectionTrait, C: ConnectionTrait,
{ {
let am = ActiveModelBehavior::before_save(self, true)?; let am = ActiveModelBehavior::before_save(self, db, true).await?;
let model = <Self::Entity as EntityTrait>::insert(am) let model = <Self::Entity as EntityTrait>::insert(am)
.exec_with_returning(db) .exec_with_returning(db)
.await?; .await?;
Self::after_save(model, true) Self::after_save(model, db, true).await
} }
/// Perform the `UPDATE` operation on an ActiveModel /// Perform the `UPDATE` operation on an ActiveModel
@ -413,9 +413,9 @@ pub trait ActiveModelTrait: Clone + Debug {
Self: ActiveModelBehavior + 'a, Self: ActiveModelBehavior + 'a,
C: ConnectionTrait, C: ConnectionTrait,
{ {
let am = ActiveModelBehavior::before_save(self, false)?; let am = ActiveModelBehavior::before_save(self, db, false).await?;
let model: <Self::Entity as EntityTrait>::Model = Self::Entity::update(am).exec(db).await?; let model: <Self::Entity as EntityTrait>::Model = Self::Entity::update(am).exec(db).await?;
Self::after_save(model, false) Self::after_save(model, db, false).await
} }
/// Insert the model if primary key is `NotSet`, update otherwise. /// Insert the model if primary key is `NotSet`, update otherwise.
@ -490,10 +490,10 @@ pub trait ActiveModelTrait: Clone + Debug {
Self: ActiveModelBehavior + 'a, Self: ActiveModelBehavior + 'a,
C: ConnectionTrait, C: ConnectionTrait,
{ {
let am = ActiveModelBehavior::before_delete(self)?; let am = ActiveModelBehavior::before_delete(self, db).await?;
let am_clone = am.clone(); let am_clone = am.clone();
let delete_res = Self::Entity::delete(am).exec(db).await?; let delete_res = Self::Entity::delete(am).exec(db).await?;
ActiveModelBehavior::after_delete(am_clone)?; ActiveModelBehavior::after_delete(am_clone, db).await?;
Ok(delete_res) Ok(delete_res)
} }
@ -597,6 +597,7 @@ pub trait ActiveModelTrait: Clone + Debug {
/// ``` /// ```
/// See module level docs [crate::entity] for a full example /// See module level docs [crate::entity] for a full example
#[allow(unused_variables)] #[allow(unused_variables)]
#[async_trait]
pub trait ActiveModelBehavior: ActiveModelTrait { pub trait ActiveModelBehavior: ActiveModelTrait {
/// Create a new ActiveModel with default values. Also used by `Default::default()`. /// Create a new ActiveModel with default values. Also used by `Default::default()`.
fn new() -> Self { fn new() -> Self {
@ -604,25 +605,38 @@ pub trait ActiveModelBehavior: ActiveModelTrait {
} }
/// Will be called before saving /// Will be called before saving
fn before_save(self, insert: bool) -> Result<Self, DbErr> { async fn before_save<C>(self, db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
Ok(self) Ok(self)
} }
/// Will be called after saving /// Will be called after saving
fn after_save( async fn after_save<C>(
model: <Self::Entity as EntityTrait>::Model, model: <Self::Entity as EntityTrait>::Model,
db: &C,
insert: bool, insert: bool,
) -> Result<<Self::Entity as EntityTrait>::Model, DbErr> { ) -> Result<<Self::Entity as EntityTrait>::Model, DbErr>
where
C: ConnectionTrait,
{
Ok(model) Ok(model)
} }
/// Will be called before deleting /// Will be called before deleting
fn before_delete(self) -> Result<Self, DbErr> { async fn before_delete<C>(self, db: &C) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
Ok(self) Ok(self)
} }
/// Will be called after deleting /// Will be called after deleting
fn after_delete(self) -> Result<Self, DbErr> { async fn after_delete<C>(self, db: &C) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
Ok(self) Ok(self)
} }
} }

View File

@ -1,4 +1,4 @@
use sea_orm::entity::prelude::*; use sea_orm::{entity::prelude::*, ConnectionTrait};
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "cake")] #[sea_orm(table_name = "cake")]
@ -49,6 +49,7 @@ impl Related<super::lineitem::Entity> for Entity {
} }
} }
#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel { impl ActiveModelBehavior for ActiveModel {
fn new() -> Self { fn new() -> Self {
use sea_orm::Set; use sea_orm::Set;
@ -58,7 +59,10 @@ impl ActiveModelBehavior for ActiveModel {
} }
} }
fn before_save(self, insert: bool) -> Result<Self, DbErr> { async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
use rust_decimal_macros::dec; use rust_decimal_macros::dec;
if self.price.as_ref() == &dec!(0) { if self.price.as_ref() == &dec!(0) {
Err(DbErr::Custom(format!( Err(DbErr::Custom(format!(
@ -70,7 +74,10 @@ impl ActiveModelBehavior for ActiveModel {
} }
} }
fn after_save(model: Model, insert: bool) -> Result<Model, DbErr> { async fn after_save<C>(model: Model, _db: &C, insert: bool) -> Result<Model, DbErr>
where
C: ConnectionTrait,
{
use rust_decimal_macros::dec; use rust_decimal_macros::dec;
if model.price < dec!(0) { if model.price < dec!(0) {
Err(DbErr::Custom(format!( Err(DbErr::Custom(format!(
@ -82,7 +89,10 @@ impl ActiveModelBehavior for ActiveModel {
} }
} }
fn before_delete(self) -> Result<Self, DbErr> { async fn before_delete<C>(self, _db: &C) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
if self.name.as_ref().contains("(err_on_before_delete)") { if self.name.as_ref().contains("(err_on_before_delete)") {
Err(DbErr::Custom( Err(DbErr::Custom(
"[before_delete] Cannot be deleted".to_owned(), "[before_delete] Cannot be deleted".to_owned(),
@ -92,7 +102,10 @@ impl ActiveModelBehavior for ActiveModel {
} }
} }
fn after_delete(self) -> Result<Self, DbErr> { async fn after_delete<C>(self, _db: &C) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
if self.name.as_ref().contains("(err_on_after_delete)") { if self.name.as_ref().contains("(err_on_after_delete)") {
Err(DbErr::Custom("[after_delete] Cannot be deleted".to_owned())) Err(DbErr::Custom("[after_delete] Cannot be deleted".to_owned()))
} else { } else {

View File

@ -0,0 +1,15 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "edit_log")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub action: String,
pub values: Json,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -4,6 +4,7 @@ pub mod applog;
pub mod byte_primary_key; pub mod byte_primary_key;
pub mod collection; pub mod collection;
pub mod custom_active_model; pub mod custom_active_model;
pub mod edit_log;
pub mod event_trigger; pub mod event_trigger;
pub mod insert_default; pub mod insert_default;
pub mod json_struct; pub mod json_struct;
@ -23,6 +24,7 @@ pub use active_enum_child::Entity as ActiveEnumChild;
pub use applog::Entity as Applog; pub use applog::Entity as Applog;
pub use byte_primary_key::Entity as BytePrimaryKey; pub use byte_primary_key::Entity as BytePrimaryKey;
pub use collection::Entity as Collection; pub use collection::Entity as Collection;
pub use edit_log::Entity as EditLog;
pub use event_trigger::Entity as EventTrigger; pub use event_trigger::Entity as EventTrigger;
pub use insert_default::Entity as InsertDefault; pub use insert_default::Entity as InsertDefault;
pub use json_struct::Entity as JsonStruct; pub use json_struct::Entity as JsonStruct;

View File

@ -1,6 +1,8 @@
use sea_orm::entity::prelude::*; use super::edit_log;
use sea_orm::{entity::prelude::*, ConnectionTrait, Set, TryIntoModel};
use serde::Serialize;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "repository")] #[sea_orm(table_name = "repository")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key, auto_increment = false)] #[sea_orm(primary_key, auto_increment = false)]
@ -13,4 +15,57 @@ pub struct Model {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {} pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {} #[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {
async fn before_save<C>(self, db: &C, _: bool) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
let model = self.clone().try_into_model()?;
insert_edit_log("before_save", &model, db).await?;
Ok(self)
}
async fn after_save<C>(model: Model, db: &C, _: bool) -> Result<Model, DbErr>
where
C: ConnectionTrait,
{
insert_edit_log("after_save", &model, db).await?;
Ok(model)
}
async fn before_delete<C>(self, db: &C) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
let model = self.clone().try_into_model()?;
insert_edit_log("before_delete", &model, db).await?;
Ok(self)
}
async fn after_delete<C>(self, db: &C) -> Result<Self, DbErr>
where
C: ConnectionTrait,
{
let model = self.clone().try_into_model()?;
insert_edit_log("after_delete", &model, db).await?;
Ok(self)
}
}
async fn insert_edit_log<T, M, C>(action: T, model: &M, db: &C) -> Result<(), DbErr>
where
T: Into<String>,
M: Serialize,
C: ConnectionTrait,
{
edit_log::ActiveModel {
action: Set(action.into()),
values: Set(serde_json::json!(model)),
..Default::default()
}
.insert(db)
.await?;
Ok(())
}

View File

@ -43,6 +43,7 @@ pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> {
create_insert_default_table(db).await?; create_insert_default_table(db).await?;
create_pi_table(db).await?; create_pi_table(db).await?;
create_uuid_fmt_table(db).await?; create_uuid_fmt_table(db).await?;
create_edit_log_table(db).await?;
if DbBackend::Postgres == db_backend { if DbBackend::Postgres == db_backend {
create_collection_table(db).await?; create_collection_table(db).await?;
@ -480,3 +481,20 @@ pub async fn create_uuid_fmt_table(db: &DbConn) -> Result<ExecResult, DbErr> {
create_table(db, &stmt, UuidFmt).await create_table(db, &stmt, UuidFmt).await
} }
pub async fn create_edit_log_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
.table(edit_log::Entity)
.col(
ColumnDef::new(edit_log::Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(edit_log::Column::Action).string().not_null())
.col(ColumnDef::new(edit_log::Column::Values).json().not_null())
.to_owned();
create_table(db, &stmt, EditLog).await
}

View File

@ -3,6 +3,7 @@ pub mod common;
pub use common::{features::*, setup::*, TestContext}; pub use common::{features::*, setup::*, TestContext};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use sea_orm::{entity::prelude::*, entity::*, DatabaseConnection}; use sea_orm::{entity::prelude::*, entity::*, DatabaseConnection};
use serde_json::json;
#[sea_orm_macros::test] #[sea_orm_macros::test]
#[cfg(any( #[cfg(any(
@ -14,13 +15,13 @@ async fn main() -> Result<(), DbErr> {
let ctx = TestContext::new("features_schema_string_primary_key_tests").await; let ctx = TestContext::new("features_schema_string_primary_key_tests").await;
create_tables(&ctx.db).await?; create_tables(&ctx.db).await?;
create_and_update_repository(&ctx.db).await?; create_and_update_repository(&ctx.db).await?;
insert_repository(&ctx.db).await?; insert_and_delete_repository(&ctx.db).await?;
ctx.delete().await; ctx.delete().await;
Ok(()) Ok(())
} }
pub async fn insert_repository(db: &DatabaseConnection) -> Result<(), DbErr> { pub async fn insert_and_delete_repository(db: &DatabaseConnection) -> Result<(), DbErr> {
let repository = repository::Model { let repository = repository::Model {
id: "unique-id-001".to_owned(), id: "unique-id-001".to_owned(),
owner: "GC".to_owned(), owner: "GC".to_owned(),
@ -41,6 +42,54 @@ pub async fn insert_repository(db: &DatabaseConnection) -> Result<(), DbErr> {
} }
); );
result.delete(db).await?;
assert_eq!(
edit_log::Entity::find().all(db).await?,
[
edit_log::Model {
id: 1,
action: "before_save".into(),
values: json!({
"description": null,
"id": "unique-id-001",
"name": "G.C.",
"owner": "GC",
}),
},
edit_log::Model {
id: 2,
action: "after_save".into(),
values: json!({
"description": null,
"id": "unique-id-001",
"name": "G.C.",
"owner": "GC",
}),
},
edit_log::Model {
id: 3,
action: "before_delete".into(),
values: json!({
"description": null,
"id": "unique-id-001",
"name": "G.C.",
"owner": "GC",
}),
},
edit_log::Model {
id: 4,
action: "after_delete".into(),
values: json!({
"description": null,
"id": "unique-id-001",
"name": "G.C.",
"owner": "GC",
}),
},
]
);
Ok(()) Ok(())
} }