Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move find_all to entity_api #15

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions entity_api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"

[dependencies]
entity = { path = "../entity" }
service = { path = "../service" }
serde_json = "1.0.107"
log = "0.4.20"

Expand Down
66 changes: 66 additions & 0 deletions entity_api/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::error::Error as StdError;
use std::fmt;

use sea_orm::error::DbErr;

/// Errors while executing operations related to entities.
/// The intent is to categorize errors into two major types:
/// * Errors related to data. Ex DbError::RecordNotFound
/// * Errors related to interactions with the database itself. Ex DbError::Conn
#[derive(Debug)]
pub struct Error {
// Underlying error emitted from seaORM internals
pub inner: DbErr,
// Enum representing which category of error
pub error_type: EntityApiError,
}

#[derive(Debug)]
pub enum EntityApiError {
DatabaseConnectionLost,
// Record not found
RecordNotFound,
// Record not updated
RecordNotUpdated,
// Errors related to interactions with the database itself. Ex DbError::Conn
SystemError,
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Entity API Error: {:?}", self)
}
}

impl StdError for Error {}

impl From<DbErr> for Error {
fn from(err: DbErr) -> Self {
match err {
DbErr::RecordNotFound(_) => Error {
inner: err,
error_type: EntityApiError::RecordNotFound,
},
DbErr::RecordNotUpdated => Error {
inner: err,
error_type: EntityApiError::RecordNotUpdated,
},
DbErr::ConnectionAcquire(_) => Error {
inner: err,
error_type: EntityApiError::SystemError,
},
DbErr::Conn(_) => Error {
inner: err,
error_type: EntityApiError::DatabaseConnectionLost,
},
DbErr::Exec(_) => Error {
inner: err,
error_type: EntityApiError::SystemError,
},
_ => Error {
inner: err,
error_type: EntityApiError::SystemError,
},
}
}
}
7 changes: 4 additions & 3 deletions entity_api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use sea_orm::DatabaseConnection;
use service::AppState;

pub mod error;
pub mod organization;

pub async fn seed_database(db: &DatabaseConnection) {
organization::seed_database(db).await;
pub async fn seed_database(app_state: &AppState) {
organization::seed_database(app_state).await;
}
19 changes: 17 additions & 2 deletions entity_api/src/organization.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
use super::error::Error;
use entity::organization;
use sea_orm::{entity::prelude::*, ActiveValue, DatabaseConnection};
use organization::{Entity, Model};
use sea_orm::{entity::prelude::*, ActiveValue};
use serde_json::json;
use service::AppState;

pub(crate) async fn seed_database(db: &DatabaseConnection) {
pub async fn find_all(app_state: &AppState) -> Result<Vec<Model>, Error> {
let db = app_state.database_connection.as_ref().unwrap();
Ok(Entity::find().all(db).await?)
}

pub async fn find_by_id(app_state: &AppState, id: i32) -> Result<Option<Model>, Error> {
let db = app_state.database_connection.as_ref().unwrap();
Ok(Entity::find_by_id(id).one(db).await?)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this a lot. I'm curious, though, if we're now only transforming the error up one level in the actual controller? I think we'd need to write a test to determine this.

Copy link
Member

@jhodapp jhodapp Dec 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you figured this out better in your other branch. Wrapping in Ok() is a valid solution because the Result<> is unwrapped from await(), but we need it to be in a Result<> for both the Ok or Err cases.

But having a naked ? operator is always going to be my favorite solution syntactically, so I'm excited to see what you came up with.

}

pub(crate) async fn seed_database(app_state: &AppState) {
let organization_names = [
"Jim Hodapp Coaching",
"Caleb Coaching",
"Enterprise Software",
];

let db = app_state.database_connection.as_ref().unwrap();

for name in organization_names {
let organization = organization::ActiveModel::from_json(json!({
"name": name,
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ async fn main() {
let mut app_state = AppState::new(config);
app_state = service::init_database(app_state).await.unwrap();

entity_api::seed_database(app_state.database_connection.as_ref().unwrap()).await;
entity_api::seed_database(&app_state).await;

web::init_server(app_state).await.unwrap();
}
Expand Down
2 changes: 2 additions & 0 deletions web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ edition = "2021"

[dependencies]
entity = { path = "../entity" }
entity_api = { path = "../entity_api" }
service = { path = "../service" }

axum = "0.6.20"
log = "0.4"
tower-http = { version = "0.4.4", features = ["fs"] }
serde_json = "1.0.107"
serde = { version = "1.0", features = ["derive"] }

[dependencies.sea-orm]
version = "0.12.3" # sea-orm version
Expand Down
14 changes: 7 additions & 7 deletions web/src/controller/organization_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use axum::response::IntoResponse;
use axum::Json;
use entity::organization;
use entity::organization::Entity as Organization;
use entity_api::organization as OrganizationApi;
use sea_orm::entity::EntityTrait;
use sea_orm::ActiveModelTrait;
use sea_orm::ActiveValue::{NotSet, Set};
Expand All @@ -21,10 +22,9 @@ impl OrganizationController {
/// --request GET \
/// http://localhost:4000/organizations
pub async fn index(State(app_state): State<AppState>) -> impl IntoResponse {
let organizations = organization::Entity::find()
.all(&app_state.database_connection.unwrap())
let organizations = OrganizationApi::find_all(&app_state)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I prefer keeping the unwrap_or_default detail inside of the entity_api context since to me it's an implementation detail and will always be the behavior we're expecting with a find_all. Open to your thoughts on this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's ok, this was only recognizing that unwrap_or(vec![]) vs unwrap_or_default() should return the same thing. But if you think moving any unwrap into the EntityAPI for Organization is even stronger, I'm good with that. Let's take a look at this further together.

.await
.unwrap_or(vec![]);
.unwrap_or_default();

Json(organizations)
}
Expand All @@ -36,10 +36,10 @@ impl OrganizationController {
pub async fn read(State(app_state): State<AppState>, Path(id): Path<i32>) -> impl IntoResponse {
debug!("GET Organization by id: {}", id);

let organization: Option<organization::Model> = organization::Entity::find_by_id(id)
.one(&app_state.database_connection.unwrap())
.await
.unwrap_or_default();
let organization: Result<Option<organization::Model>, Error> =
OrganizationApi::find_by_id(&app_state, id)
.await
.map_err(|err| err.into());

Json(organization)
}
Expand Down
38 changes: 34 additions & 4 deletions web/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,57 @@
use std::error::Error as StdError;

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Serialize;

use entity_api::error::EntityApiError;
use entity_api::error::Error as EntityApiErrorSuper;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's talk through this when we meet later today. I think you've definitely exposed a part of how this is set up that could use more thought. I'm not sure I love adding the Super since to me that implies inheritance or nesting. But I also don't have a better option in mind off-hand.

Copy link
Member

@jhodapp jhodapp Dec 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I borrowed the Super nomenclature from Rust modules which I think makes this fair. My approach also localizes the name distinction to this module only instead of EntityApiErrorType having "Type" always. From my understanding of Rust convention in the community, adding "Type" to the end of a type is not a common practice. It is in C/C++ land, but not Rust.


pub type Result<T> = core::result::Result<T, Error>;

#[derive(Debug)]
#[derive(Debug, Serialize)]

pub enum Error {
DatabaseConnectionLost,
InternalServer,
EntityNotFound,
UnprocessableEntity,
}

impl StdError for Error {}

impl std::fmt::Display for Error {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> core::result::Result<(), std::fmt::Error> {
write!(fmt, "{self:?}")
}
}

impl IntoResponse for Error {
fn into_response(self) -> Response {
match self {
Error::DatabaseConnectionLost => (
StatusCode::INTERNAL_SERVER_ERROR,
"DB CONNECTION LOST - INTERNAL SERVER ERROR",
)
.into_response(),
Error::InternalServer => {
(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL SERVER ERROR").into_response()
}
Error::EntityNotFound => (StatusCode::NOT_FOUND, "ENTITY NOT FOUND").into_response(),
Error::UnprocessableEntity => {
(StatusCode::UNPROCESSABLE_ENTITY, "UNPROCESSABLE ENTITY").into_response()
}
}
}
}

impl std::fmt::Display for Error {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> core::result::Result<(), std::fmt::Error> {
write!(fmt, "{self:?}")
impl From<EntityApiErrorSuper> for Error {
fn from(err: EntityApiErrorSuper) -> Self {
match err.error_type {
EntityApiError::DatabaseConnectionLost => Error::DatabaseConnectionLost,
EntityApiError::RecordNotFound => Error::EntityNotFound,
EntityApiError::RecordNotUpdated => Error::UnprocessableEntity,
EntityApiError::SystemError => Error::InternalServer,
}
}
}