From e78195cfac967679d21366b77eb9db68fcac4b2e Mon Sep 17 00:00:00 2001 From: simonsan <14062932+simonsan@users.noreply.github.com> Date: Wed, 3 Apr 2024 01:21:40 +0200 Subject: [PATCH] add repositories Signed-off-by: simonsan <14062932+simonsan@users.noreply.github.com> --- crates/error/src/lib.rs | 20 +++++ .../m20240325_000001_create_activities.rs | 20 +++++ .../migration/m20240326_000001_create_tags.rs | 10 +++ .../m20240326_000002_create_categories.rs | 10 +++ crates/storage/src/repository.rs | 35 ++++++-- crates/storage/src/repository/activity.rs | 81 +++++++++++++++++++ crates/storage/src/repository/category.rs | 79 ++++++++++++++---- crates/storage/src/repository/tag.rs | 81 +++++++++++++++++++ 8 files changed, 315 insertions(+), 21 deletions(-) create mode 100644 crates/storage/src/repository/activity.rs create mode 100644 crates/storage/src/repository/tag.rs diff --git a/crates/error/src/lib.rs b/crates/error/src/lib.rs index 18fa23d..6fbda32 100644 --- a/crates/error/src/lib.rs +++ b/crates/error/src/lib.rs @@ -293,6 +293,26 @@ pub enum DatabaseStorageErrorKind { #[source] source: sea_orm::error::SqlErr, }, + + /// Failed to read item {item_type}::{item_id} from database: {source} + RepositoryReadFailed { + source: sea_orm::DbErr, + item_type: String, + item_id: String, + }, + + /// Failed to delete item {item_type}::{item_id} from database: {source} + RepositoryDeleteFailed { + source: sea_orm::prelude::DbErr, + item_type: String, + item_id: String, + }, + + /// Failed to create item {item_type} in database: {source} + RepositoryCreateFailed { + source: sea_orm::prelude::DbErr, + item_type: String, + }, } /// [`TomlFileStorageErrorKind`] describes the errors that can happen while dealing with the Toml file storage. diff --git a/crates/storage/src/migration/m20240325_000001_create_activities.rs b/crates/storage/src/migration/m20240325_000001_create_activities.rs index c1b8e6f..5105ad7 100644 --- a/crates/storage/src/migration/m20240325_000001_create_activities.rs +++ b/crates/storage/src/migration/m20240325_000001_create_activities.rs @@ -45,6 +45,26 @@ impl MigrationTrait for Migration { ) .to_owned(), ) + .await?; + + manager + .create_index( + Index::create() + .table(Activities::Table) + .name("idx_activities_parent_guid") + .col(Activities::ParentGuid) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .table(Activities::Table) + .name("idx_activities_description") + .col(Activities::Description) + .to_owned(), + ) .await } diff --git a/crates/storage/src/migration/m20240326_000001_create_tags.rs b/crates/storage/src/migration/m20240326_000001_create_tags.rs index b6ada7d..38493bd 100644 --- a/crates/storage/src/migration/m20240326_000001_create_tags.rs +++ b/crates/storage/src/migration/m20240326_000001_create_tags.rs @@ -17,6 +17,16 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Tags::Tag).text().not_null()) .to_owned(), ) + .await?; + + manager + .create_index( + Index::create() + .table(Tags::Table) + .name("idx_tags_tag") + .col(Tags::Tag) + .to_owned(), + ) .await } diff --git a/crates/storage/src/migration/m20240326_000002_create_categories.rs b/crates/storage/src/migration/m20240326_000002_create_categories.rs index 1c91730..bf6ad4a 100644 --- a/crates/storage/src/migration/m20240326_000002_create_categories.rs +++ b/crates/storage/src/migration/m20240326_000002_create_categories.rs @@ -23,6 +23,16 @@ impl MigrationTrait for Migration { .col(ColumnDef::new(Categories::Description).text().null()) .to_owned(), ) + .await?; + + manager + .create_index( + Index::create() + .table(Categories::Table) + .name("idx_categories_category") + .col(Categories::Category) + .to_owned(), + ) .await } diff --git a/crates/storage/src/repository.rs b/crates/storage/src/repository.rs index 5159dc9..e4373f5 100644 --- a/crates/storage/src/repository.rs +++ b/crates/storage/src/repository.rs @@ -1,8 +1,10 @@ +pub mod activity; pub mod category; +pub mod tag; use pace_error::{PaceOptResult, PaceResult}; -pub trait Repository { +pub(crate) trait Repository { /// Read a single entity by its id. /// /// # Arguments @@ -16,7 +18,7 @@ pub trait Repository { /// # Returns /// /// Returns the entity if it exists or None if it does not. - fn read(&self, id: &str) -> PaceOptResult; + async fn read(&self, id: &str) -> PaceOptResult; /// Read all entities of a given type. /// @@ -28,7 +30,7 @@ pub trait Repository { /// /// Returns a vector of all entities of the given type or an /// empty vector if there are none. - fn read_all(&self) -> PaceOptResult>; + async fn read_all(&self) -> PaceOptResult>; /// Create a new entity of a given type. /// @@ -43,7 +45,7 @@ pub trait Repository { /// # Returns /// /// Returns the id of the created entity. - fn create(&self, entity: &T) -> PaceResult; + async fn create(&self, model: &T) -> PaceResult; /// Update an existing entity of a given type. /// @@ -59,7 +61,7 @@ pub trait Repository { /// # Returns /// /// Returns nothing if the entity was updated successfully. - fn update(&self, id: &str, entity: &T) -> PaceResult<()>; + async fn update(&self, id: &str, model: &T) -> PaceResult<()>; /// Delete an existing entity of a given type. /// @@ -70,5 +72,26 @@ pub trait Repository { /// # Errors /// /// Returns an error if there was a problem deleting the entity. - fn delete(&self, id: &str) -> PaceResult<()>; + /// + /// # Returns + /// + /// Returns the deleted entity if it exists. + async fn delete(&self, id: &str) -> PaceOptResult; +} + +pub struct SeaOrmRepository<'conn> { + activity: activity::ActivityRepository<'conn, sea_orm::DatabaseConnection>, + category: category::CategoryRepository<'conn, sea_orm::DatabaseConnection>, + tag: tag::TagRepository<'conn, sea_orm::DatabaseConnection>, +} + +impl<'conn> SeaOrmRepository<'conn> { + #[must_use] + pub const fn new(connection: &'conn sea_orm::DatabaseConnection) -> Self { + Self { + activity: activity::ActivityRepository::new(connection), + category: category::CategoryRepository::new(connection), + tag: tag::TagRepository::new(connection), + } + } } diff --git a/crates/storage/src/repository/activity.rs b/crates/storage/src/repository/activity.rs new file mode 100644 index 0000000..ef06e55 --- /dev/null +++ b/crates/storage/src/repository/activity.rs @@ -0,0 +1,81 @@ +use pace_error::{DatabaseStorageErrorKind, PaceOptResult, PaceResult}; +use sea_orm::{EntityTrait, IntoActiveModel}; + +use crate::entity::activities::{Entity as ActivityEntity, Model as ActivityModel}; +use crate::repository::Repository; + +pub struct ActivityRepository<'conn, C> { + connection: &'conn C, +} + +impl<'conn, C> ActivityRepository<'conn, C> { + pub const fn new(connection: &'conn C) -> Self { + Self { connection } + } +} + +impl<'conn> Repository for ActivityRepository<'conn, sea_orm::DatabaseConnection> { + async fn read(&self, id: &str) -> PaceOptResult { + Ok(ActivityEntity::find_by_id(id) + .one(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "activity".to_string(), + item_id: id.to_string(), + })?) + } + + async fn read_all(&self) -> PaceOptResult> { + let items = ActivityEntity::find() + .all(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "activity".to_string(), + item_id: "all".to_string(), + })?; + + if items.is_empty() { + return Ok(None); + } + + Ok(Some(items)) + } + + async fn create(&self, model: &ActivityModel) -> PaceResult { + // TODO: What else should we do with ActiveModel here? + let active_model = model.clone().into_active_model(); + + let id = ActivityEntity::insert(active_model) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryCreateFailed { + source, + item_type: "activity".to_string(), + })? + .last_insert_id; + + Ok(id) + } + + async fn update(&self, id: &str, model: &ActivityModel) -> PaceResult<()> { + unimplemented!() + } + + async fn delete(&self, id: &str) -> PaceOptResult { + let item = self.read(id).await?; + + // TODO: Unused result here, what should we do with the rows affected? + _ = ActivityEntity::delete_by_id(id) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryDeleteFailed { + source, + item_type: "activity".to_string(), + item_id: id.to_string(), + })?; + + Ok(item) + } +} diff --git a/crates/storage/src/repository/category.rs b/crates/storage/src/repository/category.rs index 5d064af..bdb70c5 100644 --- a/crates/storage/src/repository/category.rs +++ b/crates/storage/src/repository/category.rs @@ -1,32 +1,81 @@ -use pace_error::{PaceOptResult, PaceResult}; -use sea_orm::{sea_query::Query, DatabaseConnection}; +use pace_error::{DatabaseStorageErrorKind, PaceOptResult, PaceResult}; +use sea_orm::{EntityTrait, IntoActiveModel}; use crate::entity::categories::{Entity as CategoryEntity, Model as CategoryModel}; -use crate::entity::tags::Entity as TagEntity; use crate::repository::Repository; -pub struct CategoryRepository { - connection: DatabaseConnection, +pub struct CategoryRepository<'conn, C> { + connection: &'conn C, } -impl Repository for CategoryRepository { - fn read(&self, id: &str) -> PaceOptResult { - unimplemented!() +impl<'conn, C> CategoryRepository<'conn, C> { + pub const fn new(connection: &'conn C) -> Self { + Self { connection } } +} - fn read_all(&self) -> PaceOptResult> { - unimplemented!() +impl<'conn> Repository for CategoryRepository<'conn, sea_orm::DatabaseConnection> { + async fn read(&self, id: &str) -> PaceOptResult { + Ok(CategoryEntity::find_by_id(id) + .one(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "category".to_string(), + item_id: id.to_string(), + })?) } - fn create(&self, entity: &CategoryEntity) -> PaceResult { - unimplemented!() + async fn read_all(&self) -> PaceOptResult> { + let items = CategoryEntity::find() + .all(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "category".to_string(), + item_id: "all".to_string(), + })?; + + if items.is_empty() { + return Ok(None); + } + + Ok(Some(items)) } - fn update(&self, id: &str, entity: &CategoryEntity) -> PaceResult<()> { - unimplemented!() + async fn create(&self, model: &CategoryModel) -> PaceResult { + // TODO: What else should we do with ActiveModel here? + let active_model = model.clone().into_active_model(); + + let id = CategoryEntity::insert(active_model) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryCreateFailed { + source, + item_type: "category".to_string(), + })? + .last_insert_id; + + Ok(id) } - fn delete(&self, id: &str) -> PaceResult<()> { + async fn update(&self, id: &str, model: &CategoryModel) -> PaceResult<()> { unimplemented!() } + + async fn delete(&self, id: &str) -> PaceOptResult { + let item = self.read(id).await?; + + // TODO: Unused result here, what should we do with the rows affected? + _ = CategoryEntity::delete_by_id(id) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryDeleteFailed { + source, + item_type: "category".to_string(), + item_id: id.to_string(), + })?; + + Ok(item) + } } diff --git a/crates/storage/src/repository/tag.rs b/crates/storage/src/repository/tag.rs new file mode 100644 index 0000000..a6502e3 --- /dev/null +++ b/crates/storage/src/repository/tag.rs @@ -0,0 +1,81 @@ +use pace_error::{DatabaseStorageErrorKind, PaceOptResult, PaceResult}; +use sea_orm::{EntityTrait, IntoActiveModel}; + +use crate::entity::tags::{Entity as TagEntity, Model as TagModel}; +use crate::repository::Repository; + +pub struct TagRepository<'conn, C> { + connection: &'conn C, +} + +impl<'conn, C> TagRepository<'conn, C> { + pub const fn new(connection: &'conn C) -> Self { + Self { connection } + } +} + +impl<'conn> Repository for TagRepository<'conn, sea_orm::DatabaseConnection> { + async fn read(&self, id: &str) -> PaceOptResult { + Ok(TagEntity::find_by_id(id) + .one(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "tag".to_string(), + item_id: id.to_string(), + })?) + } + + async fn read_all(&self) -> PaceOptResult> { + let items = TagEntity::find() + .all(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryReadFailed { + source, + item_type: "tag".to_string(), + item_id: "all".to_string(), + })?; + + if items.is_empty() { + return Ok(None); + } + + Ok(Some(items)) + } + + async fn create(&self, model: &TagModel) -> PaceResult { + // TODO: What else should we do with ActiveModel here? + let active_model = model.clone().into_active_model(); + + let id = TagEntity::insert(active_model) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryCreateFailed { + source, + item_type: "tag".to_string(), + })? + .last_insert_id; + + Ok(id) + } + + async fn update(&self, id: &str, model: &TagModel) -> PaceResult<()> { + unimplemented!() + } + + async fn delete(&self, id: &str) -> PaceOptResult { + let item = self.read(id).await?; + + // TODO: Unused result here, what should we do with the rows affected? + _ = TagEntity::delete_by_id(id) + .exec(self.connection) + .await + .map_err(|source| DatabaseStorageErrorKind::RepositoryDeleteFailed { + source, + item_type: "tag".to_string(), + item_id: id.to_string(), + })?; + + Ok(item) + } +}