diff --git a/docs/db/refactor_platform_rs.dbml b/docs/db/refactor_platform_rs.dbml index a2bfda9..7cb04e4 100644 --- a/docs/db/refactor_platform_rs.dbml +++ b/docs/db/refactor_platform_rs.dbml @@ -44,9 +44,10 @@ Table refactor_platform.coaching_sessions { Table refactor_platform.overarching_goals { id uuid [primary key, unique, not null, default: `gen_random_uuid()`] + user_id uuid [not null, note: 'User that created (owns) the overarching goal'] coaching_session_id uuid [note: 'The coaching session that an overarching goal is associated with'] title varchar [note: 'A short description of an overarching goal'] - details varchar [note: 'A long description of an overarching goal'] + body varchar [note: 'Main text of the overarching goal supporting Markdown'] completed_at timestamptz [note: 'The date and time an overarching goal was completed'] created_at timestamptz [not null, default: `now()`] updated_at timestamptz [not null, default: `now()`, note: 'The last date and time fields were changed'] @@ -64,8 +65,10 @@ Table refactor_platform.notes { Table refactor_platform.agreements { id uuid [primary key, unique, not null, default: `gen_random_uuid()`] coaching_session_id uuid [not null] - details varchar [note: 'Either a short or long description of an agreement reached between coach and coachee in a coaching session'] + body varchar [note: 'Either a short or long description of an agreement reached between coach and coachee in a coaching session'] user_id uuid [not null, note: 'User that created (owns) the agreement'] + status status [not null] + status_changed_at timestamptz created_at timestamptz [not null, default: `now()`] updated_at timestamptz [not null, default: `now()`, note: 'The last date and time an overarching agreement\'s fields were changed'] } @@ -76,13 +79,21 @@ Table refactor_platform.actions { // It will carry forward to every future session until // its due_by is passed or it was completed by the coachee coaching_session_id uuid [not null] + body varchar [note: 'Main text of the action supporting Markdown'] + user_id uuid [not null, note: 'User that created (owns) the action'] due_by timestamptz - completed boolean // May be unnecessary if there's a valid completed_at timestamp - completed_at timestamptz + status status [not null] + status_changed_at timestamptz created_at timestamp [not null, default: `now()`] updated_at timestamp [not null, default: `now()`] } +enum status { + in_progress + completed + wont_do +} + // coaching_relationships relationships Ref: refactor_platform.coaching_relationships.organization_id > refactor_platform.organizations.id Ref: refactor_platform.coaching_relationships.coachee_id > refactor_platform.users.id diff --git a/entity/README.md b/entity/README.md index a1af1a5..4e6fdf2 100644 --- a/entity/README.md +++ b/entity/README.md @@ -1,6 +1,7 @@ ## Entity Schema Diagram - Definitions and Relationships -![refactor_platform_erd_may_2024](https://github.com/Jim-Hodapp-Coaching/refactor-platform-rs/assets/14064042/f7cb9197-6f24-422a-953c-f44dd01c40ec) +![Untitled (1)](https://github.com/user-attachments/assets/1eea6ba9-b689-4bcb-8b7b-1d9cf725c16c) + ## Example Data - A User as a Coach and Coachee in Two Different Organizations diff --git a/entity/src/actions.rs b/entity/src/actions.rs index 123a8c8..8fb18ac 100644 --- a/entity/src/actions.rs +++ b/entity/src/actions.rs @@ -1,19 +1,28 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3 -use crate::Id; +use crate::{status, Id}; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, ToSchema)] #[sea_orm(schema_name = "refactor_platform", table_name = "actions")] pub struct Model { + #[serde(skip_deserializing)] #[sea_orm(primary_key)] pub id: Id, pub coaching_session_id: Id, + #[serde(skip_deserializing)] + pub user_id: Id, + pub body: Option, pub due_by: Option, - pub completed: Option, - pub completed_at: Option, + #[serde(skip_deserializing)] + pub status: status::Status, + #[serde(skip_deserializing)] + pub status_changed_at: Option, + #[serde(skip_deserializing)] pub created_at: DateTimeWithTimeZone, + #[serde(skip_deserializing)] pub updated_at: DateTimeWithTimeZone, } @@ -27,6 +36,14 @@ pub enum Relation { on_delete = "NoAction" )] CoachingSessions, + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::UserId", + to = "super::users::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Users, } impl Related for Entity { @@ -35,4 +52,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/agreements.rs b/entity/src/agreements.rs index bbd1f64..7785b64 100644 --- a/entity/src/agreements.rs +++ b/entity/src/agreements.rs @@ -1,19 +1,29 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3 -use crate::Id; +use crate::{status, Id}; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, ToSchema)] +#[schema(as = entity::agreements::Model)] #[sea_orm(schema_name = "refactor_platform", table_name = "agreements")] pub struct Model { + #[serde(skip_deserializing)] #[sea_orm(primary_key)] pub id: Id, #[sea_orm(unique)] pub coaching_session_id: Id, - pub details: Option, + pub body: Option, + #[serde(skip_deserializing)] pub user_id: Id, + #[serde(skip_deserializing)] + pub status: status::Status, + #[serde(skip_deserializing)] + pub status_changed_at: Option, + #[serde(skip_deserializing)] pub created_at: DateTimeWithTimeZone, + #[serde(skip_deserializing)] pub updated_at: DateTimeWithTimeZone, } diff --git a/entity/src/lib.rs b/entity/src/lib.rs index 49eacb1..d924289 100644 --- a/entity/src/lib.rs +++ b/entity/src/lib.rs @@ -11,6 +11,7 @@ pub mod coaching_sessions; pub mod notes; pub mod organizations; pub mod overarching_goals; +pub mod status; pub mod users; /// A type alias that represents any Entity's internal id field data type. diff --git a/entity/src/notes.rs b/entity/src/notes.rs index fc40d18..3f44534 100644 --- a/entity/src/notes.rs +++ b/entity/src/notes.rs @@ -14,6 +14,7 @@ pub struct Model { pub id: Id, pub coaching_session_id: Id, pub body: Option, + #[serde(skip_deserializing)] pub user_id: Id, #[serde(skip_deserializing)] pub created_at: DateTimeWithTimeZone, diff --git a/entity/src/overarching_goals.rs b/entity/src/overarching_goals.rs index 6bbb517..85dd471 100644 --- a/entity/src/overarching_goals.rs +++ b/entity/src/overarching_goals.rs @@ -3,17 +3,24 @@ use crate::Id; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, ToSchema)] #[sea_orm(schema_name = "refactor_platform", table_name = "overarching_goals")] pub struct Model { + #[serde(skip_deserializing)] #[sea_orm(primary_key)] pub id: Id, pub coaching_session_id: Option, + #[serde(skip_deserializing)] + pub user_id: Id, pub title: Option, - pub details: Option, + pub body: Option, + #[serde(skip_deserializing)] pub completed_at: Option, + #[serde(skip_deserializing)] pub created_at: DateTimeWithTimeZone, + #[serde(skip_deserializing)] pub updated_at: DateTimeWithTimeZone, } @@ -27,6 +34,14 @@ pub enum Relation { on_delete = "NoAction" )] CoachingSessions, + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::UserId", + to = "super::users::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Users, } impl Related for Entity { @@ -35,4 +50,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/entity/src/status.rs b/entity/src/status.rs new file mode 100644 index 0000000..ac1eef3 --- /dev/null +++ b/entity/src/status.rs @@ -0,0 +1,19 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Eq, PartialEq, EnumIter, Deserialize, Serialize, DeriveActiveEnum)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "status")] +pub enum Status { + #[sea_orm(string_value = "InProgress")] + InProgress, + #[sea_orm(string_value = "Completed")] + Completed, + #[sea_orm(string_value = "WontDo")] + WontDo, +} + +impl std::default::Default for Status { + fn default() -> Self { + Self::InProgress + } +} diff --git a/entity_api/src/action.rs b/entity_api/src/action.rs new file mode 100644 index 0000000..066c78f --- /dev/null +++ b/entity_api/src/action.rs @@ -0,0 +1,206 @@ +use super::error::{EntityApiErrorCode, Error}; +use crate::uuid_parse_str; +use entity::actions::{self, ActiveModel, Entity, Model}; +use entity::Id; +use sea_orm::{ + entity::prelude::*, + ActiveValue::{Set, Unchanged}, + DatabaseConnection, TryIntoModel, +}; +use std::collections::HashMap; + +use log::*; + +pub async fn create( + db: &DatabaseConnection, + action_model: Model, + user_id: Id, +) -> Result { + debug!("New Action Model to be inserted: {:?}", action_model); + + let now = chrono::Utc::now(); + + let action_active_model: ActiveModel = ActiveModel { + coaching_session_id: Set(action_model.coaching_session_id), + user_id: Set(user_id), + due_by: Set(action_model.due_by), + body: Set(action_model.body), + created_at: Set(now.into()), + updated_at: Set(now.into()), + ..Default::default() + }; + + Ok(action_active_model.save(db).await?.try_into_model()?) +} + +pub async fn update(db: &DatabaseConnection, id: Id, model: Model) -> Result { + let result = Entity::find_by_id(id).one(db).await?; + + match result { + Some(action) => { + debug!("Existing Action model to be Updated: {:?}", action); + + let active_model: ActiveModel = ActiveModel { + id: Unchanged(model.id), + coaching_session_id: Unchanged(model.coaching_session_id), + user_id: Unchanged(model.user_id), + body: Set(model.body), + due_by: Set(model.due_by), + status: Set(model.status), + status_changed_at: Set(model.status_changed_at), + updated_at: Set(chrono::Utc::now().into()), + created_at: Unchanged(model.created_at), + }; + + Ok(active_model.update(db).await?.try_into_model()?) + } + None => { + error!("Action with id {} not found", id); + + Err(Error { + inner: None, + error_code: EntityApiErrorCode::RecordNotFound, + }) + } + } +} + +pub async fn find_by_id(db: &DatabaseConnection, id: Id) -> Result, Error> { + match Entity::find_by_id(id).one(db).await { + Ok(Some(action)) => { + debug!("Action found: {:?}", action); + + Ok(Some(action)) + } + Ok(None) => { + error!("Action with id {} not found", id); + + Err(Error { + inner: None, + error_code: EntityApiErrorCode::RecordNotFound, + }) + } + Err(err) => { + error!("Action with id {} not found and returned error {}", id, err); + Err(Error { + inner: None, + error_code: EntityApiErrorCode::RecordNotFound, + }) + } + } +} + +pub async fn find_by( + db: &DatabaseConnection, + query_params: HashMap, +) -> Result, Error> { + let mut query = Entity::find(); + + for (key, value) in query_params { + match key.as_str() { + "coaching_session_id" => { + let coaching_session_id = uuid_parse_str(&value)?; + + query = query.filter(actions::Column::CoachingSessionId.eq(coaching_session_id)); + } + _ => { + return Err(Error { + inner: None, + error_code: EntityApiErrorCode::InvalidQueryTerm, + }); + } + } + } + + Ok(query.all(db).await?) +} + +#[cfg(test)] +// We need to gate seaORM's mock feature behind conditional compilation because +// the feature removes the Clone trait implementation from seaORM's DatabaseConnection. +// see https://github.com/SeaQL/sea-orm/issues/830 +#[cfg(feature = "mock")] +mod tests { + use super::*; + use entity::{actions::Model, Id}; + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + #[tokio::test] + async fn create_returns_a_new_action_model() -> Result<(), Error> { + let now = chrono::Utc::now(); + + let action_model = Model { + id: Id::new_v4(), + user_id: Id::new_v4(), + coaching_session_id: Id::new_v4(), + body: Some("This is a action".to_owned()), + due_by: Some(now.into()), + status_changed_at: None, + status: Default::default(), + created_at: now.into(), + updated_at: now.into(), + }; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![vec![action_model.clone()]]) + .into_connection(); + + let action = create(&db, action_model.clone().into(), Id::new_v4()).await?; + + assert_eq!(action.id, action_model.id); + + Ok(()) + } + + #[tokio::test] + async fn update_returns_an_updated_action_model() -> Result<(), Error> { + let now = chrono::Utc::now(); + + let action_model = Model { + id: Id::new_v4(), + coaching_session_id: Id::new_v4(), + due_by: Some(now.into()), + body: Some("This is a action".to_owned()), + user_id: Id::new_v4(), + status_changed_at: None, + status: Default::default(), + created_at: now.into(), + updated_at: now.into(), + }; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![vec![action_model.clone()], vec![action_model.clone()]]) + .into_connection(); + + let action = update(&db, action_model.id, action_model.clone()).await?; + + assert_eq!(action.body, action_model.body); + + Ok(()) + } + + #[tokio::test] + async fn find_by_returns_all_actions_associated_with_coaching_session() -> Result<(), Error> { + let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); + let mut query_params = HashMap::new(); + let coaching_session_id = Id::new_v4(); + + query_params.insert( + "coaching_session_id".to_owned(), + coaching_session_id.to_string(), + ); + + let _ = find_by(&db, query_params).await; + + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "actions"."id", "actions"."coaching_session_id", "actions"."user_id", "actions"."body", "actions"."due_by", CAST("actions"."status" AS text), "actions"."status_changed_at", "actions"."created_at", "actions"."updated_at" FROM "refactor_platform"."actions" WHERE "actions"."coaching_session_id" = $1"#, + [coaching_session_id.into()] + )] + ); + + Ok(()) + } +} diff --git a/entity_api/src/agreement.rs b/entity_api/src/agreement.rs new file mode 100644 index 0000000..0746718 --- /dev/null +++ b/entity_api/src/agreement.rs @@ -0,0 +1,227 @@ +use super::error::{EntityApiErrorCode, Error}; +use crate::uuid_parse_str; +use entity::agreements::{self, ActiveModel, Entity, Model}; +use entity::Id; +use sea_orm::{ + entity::prelude::*, + ActiveValue::{Set, Unchanged}, + DatabaseConnection, TryIntoModel, +}; +use std::collections::HashMap; + +use log::*; + +pub async fn create( + db: &DatabaseConnection, + agreement_model: Model, + user_id: Id, +) -> Result { + debug!("New Agreement Model to be inserted: {:?}", agreement_model); + + let now = chrono::Utc::now(); + + let agreement_active_model: ActiveModel = ActiveModel { + coaching_session_id: Set(agreement_model.coaching_session_id), + body: Set(agreement_model.body), + user_id: Set(user_id), + created_at: Set(now.into()), + updated_at: Set(now.into()), + status_changed_at: Set(None), + ..Default::default() + }; + + Ok(agreement_active_model.save(db).await?.try_into_model()?) +} + +pub async fn update(db: &DatabaseConnection, id: Id, model: Model) -> Result { + let result = Entity::find_by_id(id).one(db).await?; + + match result { + Some(agreement) => { + debug!("Existing Agreement model to be Updated: {:?}", agreement); + + let active_model: ActiveModel = ActiveModel { + id: Unchanged(agreement.id), + coaching_session_id: Unchanged(agreement.coaching_session_id), + body: Set(model.body), + user_id: Unchanged(agreement.user_id), + status: Unchanged(agreement.status), + status_changed_at: Unchanged(agreement.status_changed_at), + updated_at: Set(chrono::Utc::now().into()), + created_at: Unchanged(agreement.created_at), + }; + + Ok(active_model.update(db).await?.try_into_model()?) + } + None => { + debug!("Agreement with id {} not found", id); + + Err(Error { + inner: None, + error_code: EntityApiErrorCode::RecordNotFound, + }) + } + } +} + +pub async fn find_by_id(db: &DatabaseConnection, id: Id) -> Result, Error> { + match Entity::find_by_id(id).one(db).await { + Ok(Some(agreement)) => { + debug!("Agreement found: {:?}", agreement); + + Ok(Some(agreement)) + } + Ok(None) => { + error!("Agreement with id {} not found", id); + + Err(Error { + inner: None, + error_code: EntityApiErrorCode::RecordNotFound, + }) + } + Err(err) => { + error!("Error finding Agreement with id {}: {:?}", id, err); + + Err(Error { + inner: Some(err), + error_code: EntityApiErrorCode::RecordNotFound, + }) + } + } +} + +pub async fn find_by( + db: &DatabaseConnection, + query_params: HashMap, +) -> Result, Error> { + let mut query = Entity::find(); + + for (key, value) in query_params { + match key.as_str() { + "coaching_session_id" => { + let coaching_session_id = uuid_parse_str(&value)?; + + query = query.filter(agreements::Column::CoachingSessionId.eq(coaching_session_id)); + } + _ => { + return Err(Error { + inner: None, + error_code: EntityApiErrorCode::InvalidQueryTerm, + }); + } + } + } + + Ok(query.all(db).await?) +} + +#[cfg(test)] +// We need to gate seaORM's mock feature behind conditional compilation because +// the feature removes the Clone trait implementation from seaORM's DatabaseConnection. +// see https://github.com/SeaQL/sea-orm/issues/830 +#[cfg(feature = "mock")] +mod tests { + use super::*; + use entity::{agreements::Model, Id}; + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + #[tokio::test] + async fn create_returns_a_new_agreement_model() -> Result<(), Error> { + let now = chrono::Utc::now(); + + let agreement_model = Model { + id: Id::new_v4(), + user_id: Id::new_v4(), + coaching_session_id: Id::new_v4(), + body: Some("This is a agreement".to_owned()), + status_changed_at: None, + status: Default::default(), + created_at: now.into(), + updated_at: now.into(), + }; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![vec![agreement_model.clone()]]) + .into_connection(); + + let agreement = create(&db, agreement_model.clone().into(), Id::new_v4()).await?; + + assert_eq!(agreement.id, agreement_model.id); + + Ok(()) + } + + #[tokio::test] + async fn update_returns_an_updated_agreement_model() -> Result<(), Error> { + let now = chrono::Utc::now(); + + let agreement_model = Model { + id: Id::new_v4(), + coaching_session_id: Id::new_v4(), + body: Some("This is a agreement".to_owned()), + user_id: Id::new_v4(), + status_changed_at: None, + status: Default::default(), + created_at: now.into(), + updated_at: now.into(), + }; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![agreement_model.clone()], + vec![agreement_model.clone()], + ]) + .into_connection(); + + let agreement = update(&db, agreement_model.id, agreement_model.clone()).await?; + + assert_eq!(agreement.body, agreement_model.body); + + Ok(()) + } + + #[tokio::test] + async fn find_by_id_returns_agreement_associated_with_id() -> Result<(), Error> { + let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); + let agreement_id = Id::new_v4(); + + let _ = find_by_id(&db, agreement_id).await; + + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "agreements"."id", "agreements"."coaching_session_id", "agreements"."body", "agreements"."user_id", CAST("agreements"."status" AS text), "agreements"."status_changed_at", "agreements"."created_at", "agreements"."updated_at" FROM "refactor_platform"."agreements" WHERE "agreements"."id" = $1 LIMIT $2"#, + [agreement_id.into(), sea_orm::Value::BigUnsigned(Some(1))] + )] + ); + + Ok(()) + } + + #[tokio::test] + async fn find_by_returns_all_agreements_associated_with_coaching_session() -> Result<(), Error> + { + let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); + let mut query_params = HashMap::new(); + let coaching_session_id = Id::new_v4(); + + query_params.insert( + "coaching_session_id".to_owned(), + coaching_session_id.to_string(), + ); + + let _ = find_by(&db, query_params).await; + + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "agreements"."id", "agreements"."coaching_session_id", "agreements"."body", "agreements"."user_id", CAST("agreements"."status" AS text), "agreements"."status_changed_at", "agreements"."created_at", "agreements"."updated_at" FROM "refactor_platform"."agreements" WHERE "agreements"."coaching_session_id" = $1"#, + [coaching_session_id.into()] + )] + ); + + Ok(()) + } +} diff --git a/entity_api/src/lib.rs b/entity_api/src/lib.rs index 9fd6877..19d4b93 100644 --- a/entity_api/src/lib.rs +++ b/entity_api/src/lib.rs @@ -4,11 +4,14 @@ use sea_orm::{ActiveModelTrait, DatabaseConnection, Set}; use entity::{coaching_relationships, coaching_sessions, organizations, users, Id}; +pub mod action; +pub mod agreement; pub mod coaching_relationship; pub mod coaching_session; pub mod error; pub mod note; pub mod organization; +pub mod overarching_goal; pub mod user; pub(crate) fn uuid_parse_str(uuid_str: &str) -> Result { diff --git a/entity_api/src/note.rs b/entity_api/src/note.rs index db64228..60a4e0d 100644 --- a/entity_api/src/note.rs +++ b/entity_api/src/note.rs @@ -11,7 +11,11 @@ use std::collections::HashMap; use log::*; -pub async fn create(db: &DatabaseConnection, note_model: Model) -> Result { +pub async fn create( + db: &DatabaseConnection, + note_model: Model, + user_id: Id, +) -> Result { debug!("New Note Model to be inserted: {:?}", note_model); let now = chrono::Utc::now(); @@ -19,7 +23,7 @@ pub async fn create(db: &DatabaseConnection, note_model: Model) -> Result Result { + debug!( + "New Overarching Goal Model to be inserted: {:?}", + overarching_goal_model + ); + + let now = chrono::Utc::now(); + + let overarching_goal_active_model: ActiveModel = ActiveModel { + coaching_session_id: Set(overarching_goal_model.coaching_session_id), + user_id: Set(user_id), + title: Set(overarching_goal_model.title), + body: Set(overarching_goal_model.body), + created_at: Set(now.into()), + updated_at: Set(now.into()), + ..Default::default() + }; + + Ok(overarching_goal_active_model + .save(db) + .await? + .try_into_model()?) +} + +pub async fn update(db: &DatabaseConnection, id: Id, model: Model) -> Result { + let result = Entity::find_by_id(id).one(db).await?; + + match result { + Some(overarching_goal) => { + debug!( + "Existing Overarching Goal model to be Updated: {:?}", + overarching_goal + ); + + let active_model: ActiveModel = ActiveModel { + id: Unchanged(model.id), + coaching_session_id: Unchanged(model.coaching_session_id), + user_id: Unchanged(model.user_id), + body: Set(model.body), + title: Set(model.title), + completed_at: Set(model.completed_at), + updated_at: Set(chrono::Utc::now().into()), + created_at: Unchanged(model.created_at), + }; + + Ok(active_model.update(db).await?.try_into_model()?) + } + None => { + error!("Overarching Goal with id {} not found", id); + + Err(Error { + inner: None, + error_code: EntityApiErrorCode::RecordNotFound, + }) + } + } +} + +pub async fn find_by_id(db: &DatabaseConnection, id: Id) -> Result, Error> { + match Entity::find_by_id(id).one(db).await { + Ok(Some(overarching_goal)) => { + debug!("Overarching Goal found: {:?}", overarching_goal); + + Ok(Some(overarching_goal)) + } + Ok(None) => { + error!("Overarching Goal with id {} not found", id); + + Err(Error { + inner: None, + error_code: EntityApiErrorCode::RecordNotFound, + }) + } + Err(err) => { + error!( + "Overarching Goal with id {} not found and returned error {}", + id, err + ); + Err(Error { + inner: None, + error_code: EntityApiErrorCode::RecordNotFound, + }) + } + } +} + +pub async fn find_by( + db: &DatabaseConnection, + query_params: HashMap, +) -> Result, Error> { + let mut query = Entity::find(); + + for (key, value) in query_params { + match key.as_str() { + "coaching_session_id" => { + let coaching_session_id = uuid_parse_str(&value)?; + + query = query + .filter(overarching_goals::Column::CoachingSessionId.eq(coaching_session_id)); + } + _ => { + return Err(Error { + inner: None, + error_code: EntityApiErrorCode::InvalidQueryTerm, + }); + } + } + } + + Ok(query.all(db).await?) +} + +#[cfg(test)] +// We need to gate seaORM's mock feature behind conditional compilation because +// the feature removes the Clone trait implementation from seaORM's DatabaseConnection. +// see https://github.com/SeaQL/sea-orm/issues/830 +#[cfg(feature = "mock")] +mod tests { + use super::*; + use entity::{overarching_goals::Model, Id}; + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; + + #[tokio::test] + async fn create_returns_a_new_overarching_goal_model() -> Result<(), Error> { + let now = chrono::Utc::now(); + + let overarching_goal_model = Model { + id: Id::new_v4(), + user_id: Id::new_v4(), + coaching_session_id: Some(Id::new_v4()), + title: Some("title".to_owned()), + body: Some("This is a overarching_goal".to_owned()), + completed_at: Some(now.into()), + created_at: now.into(), + updated_at: now.into(), + }; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![vec![overarching_goal_model.clone()]]) + .into_connection(); + + let overarching_goal = + create(&db, overarching_goal_model.clone().into(), Id::new_v4()).await?; + + assert_eq!(overarching_goal.id, overarching_goal_model.id); + + Ok(()) + } + + #[tokio::test] + async fn update_returns_an_updated_overarching_goal_model() -> Result<(), Error> { + let now = chrono::Utc::now(); + + let overarching_goal_model = Model { + id: Id::new_v4(), + coaching_session_id: Some(Id::new_v4()), + title: Some("title".to_owned()), + body: Some("This is a overarching_goal".to_owned()), + user_id: Id::new_v4(), + completed_at: Some(now.into()), + created_at: now.into(), + updated_at: now.into(), + }; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results(vec![ + vec![overarching_goal_model.clone()], + vec![overarching_goal_model.clone()], + ]) + .into_connection(); + + let overarching_goal = update( + &db, + overarching_goal_model.id, + overarching_goal_model.clone(), + ) + .await?; + + assert_eq!(overarching_goal.body, overarching_goal_model.body); + + Ok(()) + } + + #[tokio::test] + async fn find_by_returns_all_overarching_goals_associated_with_coaching_session( + ) -> Result<(), Error> { + let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection(); + let mut query_params = HashMap::new(); + let coaching_session_id = Id::new_v4(); + + query_params.insert( + "coaching_session_id".to_owned(), + coaching_session_id.to_string(), + ); + + let _ = find_by(&db, query_params).await; + + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "overarching_goals"."id", "overarching_goals"."coaching_session_id", "overarching_goals"."user_id", "overarching_goals"."title", "overarching_goals"."body", "overarching_goals"."completed_at", "overarching_goals"."created_at", "overarching_goals"."updated_at" FROM "refactor_platform"."overarching_goals" WHERE "overarching_goals"."coaching_session_id" = $1"#, + [coaching_session_id.into()] + )] + ); + + Ok(()) + } +} diff --git a/migration/src/refactor_platform_rs.sql b/migration/src/refactor_platform_rs.sql index 224a0ce..999624a 100644 --- a/migration/src/refactor_platform_rs.sql +++ b/migration/src/refactor_platform_rs.sql @@ -1,8 +1,14 @@ -- SQL dump generated using DBML (dbml-lang.org) -- Database: PostgreSQL --- Generated at: 2024-06-28T21:45:29.597Z +-- Generated at: 2024-08-09T18:10:25.658Z +CREATE TYPE "status" AS ENUM ( + 'in_progress', + 'completed', + 'wont_do' +); + CREATE TABLE "refactor_platform"."organizations" ( "id" uuid UNIQUE PRIMARY KEY NOT NULL DEFAULT (gen_random_uuid()), "name" varchar NOT NULL, @@ -44,9 +50,10 @@ CREATE TABLE "refactor_platform"."coaching_sessions" ( CREATE TABLE "refactor_platform"."overarching_goals" ( "id" uuid UNIQUE PRIMARY KEY NOT NULL DEFAULT (gen_random_uuid()), + "user_id" uuid NOT NULL, "coaching_session_id" uuid, "title" varchar, - "details" varchar, + "body" varchar, "completed_at" timestamptz, "created_at" timestamptz NOT NULL DEFAULT (now()), "updated_at" timestamptz NOT NULL DEFAULT (now()) @@ -64,8 +71,10 @@ CREATE TABLE "refactor_platform"."notes" ( CREATE TABLE "refactor_platform"."agreements" ( "id" uuid UNIQUE PRIMARY KEY NOT NULL DEFAULT (gen_random_uuid()), "coaching_session_id" uuid NOT NULL, - "details" varchar, + "body" varchar, "user_id" uuid NOT NULL, + "status" status NOT NULL, + "status_changed_at" timestamptz, "created_at" timestamptz NOT NULL DEFAULT (now()), "updated_at" timestamptz NOT NULL DEFAULT (now()) ); @@ -73,9 +82,11 @@ CREATE TABLE "refactor_platform"."agreements" ( CREATE TABLE "refactor_platform"."actions" ( "id" uuid UNIQUE PRIMARY KEY NOT NULL DEFAULT (gen_random_uuid()), "coaching_session_id" uuid NOT NULL, + "body" varchar, + "user_id" uuid NOT NULL, "due_by" timestamptz, - "completed" boolean, - "completed_at" timestamptz, + "status" status NOT NULL, + "status_changed_at" timestamptz, "created_at" timestamp NOT NULL DEFAULT (now()), "updated_at" timestamp NOT NULL DEFAULT (now()) ); @@ -106,11 +117,13 @@ COMMENT ON COLUMN "refactor_platform"."coaching_sessions"."timezone" IS 'The bas COMMENT ON COLUMN "refactor_platform"."coaching_sessions"."updated_at" IS 'The last date and time fields were changed'; +COMMENT ON COLUMN "refactor_platform"."overarching_goals"."user_id" IS 'User that created (owns) the overarching goal'; + COMMENT ON COLUMN "refactor_platform"."overarching_goals"."coaching_session_id" IS 'The coaching session that an overarching goal is associated with'; COMMENT ON COLUMN "refactor_platform"."overarching_goals"."title" IS 'A short description of an overarching goal'; -COMMENT ON COLUMN "refactor_platform"."overarching_goals"."details" IS 'A long description of an overarching goal'; +COMMENT ON COLUMN "refactor_platform"."overarching_goals"."body" IS 'Main text of the overarching goal supporting Markdown'; COMMENT ON COLUMN "refactor_platform"."overarching_goals"."completed_at" IS 'The date and time an overarching goal was completed'; @@ -122,12 +135,16 @@ COMMENT ON COLUMN "refactor_platform"."notes"."user_id" IS 'User that created (o COMMENT ON COLUMN "refactor_platform"."notes"."updated_at" IS 'The last date and time an overarching note''s fields were changed'; -COMMENT ON COLUMN "refactor_platform"."agreements"."details" IS 'Either a short or long description of an agreement reached between coach and coachee in a coaching session'; +COMMENT ON COLUMN "refactor_platform"."agreements"."body" IS 'Either a short or long description of an agreement reached between coach and coachee in a coaching session'; COMMENT ON COLUMN "refactor_platform"."agreements"."user_id" IS 'User that created (owns) the agreement'; COMMENT ON COLUMN "refactor_platform"."agreements"."updated_at" IS 'The last date and time an overarching agreement''s fields were changed'; +COMMENT ON COLUMN "refactor_platform"."actions"."body" IS 'Main text of the action supporting Markdown'; + +COMMENT ON COLUMN "refactor_platform"."actions"."user_id" IS 'User that created (owns) the action'; + ALTER TABLE "refactor_platform"."coaching_relationships" ADD FOREIGN KEY ("organization_id") REFERENCES "refactor_platform"."organizations" ("id"); ALTER TABLE "refactor_platform"."coaching_relationships" ADD FOREIGN KEY ("coachee_id") REFERENCES "refactor_platform"."users" ("id"); diff --git a/web/src/controller/action_controller.rs b/web/src/controller/action_controller.rs new file mode 100644 index 0000000..9448d4b --- /dev/null +++ b/web/src/controller/action_controller.rs @@ -0,0 +1,148 @@ +use crate::controller::ApiResponse; +use crate::extractors::{ + authenticated_user::AuthenticatedUser, compare_api_version::CompareApiVersion, +}; +use crate::{AppState, Error}; +use axum::extract::{Path, Query, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::Json; +use entity::{actions::Model, Id}; +use entity_api::action as ActionApi; +use service::config::ApiVersion; +use std::collections::HashMap; + +use log::*; + +/// POST create a new Action +#[utoipa::path( + post, + path = "/actions", + params(ApiVersion), + request_body = entity::actions::Model, + responses( + (status = 201, description = "Successfully Created a New Action", body = [entity::actions::Model]), + (status= 422, description = "Unprocessable Entity"), + (status = 401, description = "Unauthorized"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] + +pub async fn create( + CompareApiVersion(_v): CompareApiVersion, + AuthenticatedUser(user): AuthenticatedUser, + // TODO: create a new Extractor to authorize the user to access + // the data requested + State(app_state): State, + Json(action_model): Json, +) -> Result { + debug!("POST Create a New Action from: {:?}", action_model); + + let action = ActionApi::create(app_state.db_conn_ref(), action_model, user.id).await?; + + debug!("New Action: {:?}", action); + + Ok(Json(ApiResponse::new(StatusCode::CREATED.into(), action))) +} + +/// GET a particular Action specified by its id. +#[utoipa::path( + get, + path = "/actions/{id}", + params( + ApiVersion, + ("id" = String, Path, description = "Action id to retrieve") + ), + responses( + (status = 200, description = "Successfully retrieved a specific Action by its id", body = [entity::notes::Model]), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Action not found"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] +pub async fn read( + CompareApiVersion(_v): CompareApiVersion, + State(app_state): State, + Path(id): Path, +) -> Result { + debug!("GET Action by id: {}", id); + + let note: Option = ActionApi::find_by_id(app_state.db_conn_ref(), id).await?; + + Ok(Json(ApiResponse::new(StatusCode::OK.into(), note))) +} + +#[utoipa::path( + put, + path = "/actions/{id}", + params( + ApiVersion, + ("id" = Id, Path, description = "Id of action to update"), + ), + request_body = entity::actions::Model, + responses( + (status = 200, description = "Successfully Updated Action", body = [entity::actions::Model]), + (status = 401, description = "Unauthorized"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] +pub async fn update( + CompareApiVersion(_v): CompareApiVersion, + AuthenticatedUser(_user): AuthenticatedUser, + // TODO: create a new Extractor to authorize the user to access + // the data requested + State(app_state): State, + Path(id): Path, + Json(action_model): Json, +) -> Result { + debug!("PUT Update Action with id: {}", id); + + let action = ActionApi::update(app_state.db_conn_ref(), id, action_model).await?; + + debug!("Updated Action: {:?}", action); + + Ok(Json(ApiResponse::new(StatusCode::OK.into(), action))) +} + +#[utoipa::path( + get, + path = "/actions", + params( + ApiVersion, + ("coaching_session_id" = Option, Query, description = "Filter by coaching_session_id") + ), + responses( + (status = 200, description = "Successfully retrieved all Actions", body = [entity::actions::Model]), + (status = 401, description = "Unauthorized"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] +pub async fn index( + CompareApiVersion(_v): CompareApiVersion, + AuthenticatedUser(_user): AuthenticatedUser, + // TODO: create a new Extractor to authorize the user to access + // the data requested + State(app_state): State, + Query(params): Query>, +) -> Result { + debug!("GET all Actions"); + debug!("Filter Params: {:?}", params); + + let actions = ActionApi::find_by(app_state.db_conn_ref(), params).await?; + + debug!("Found Actions: {:?}", actions); + + Ok(Json(ApiResponse::new(StatusCode::OK.into(), actions))) +} diff --git a/web/src/controller/agreement_controller.rs b/web/src/controller/agreement_controller.rs new file mode 100644 index 0000000..db70a1f --- /dev/null +++ b/web/src/controller/agreement_controller.rs @@ -0,0 +1,151 @@ +use crate::controller::ApiResponse; +use crate::extractors::{ + authenticated_user::AuthenticatedUser, compare_api_version::CompareApiVersion, +}; +use crate::{AppState, Error}; +use axum::extract::{Path, Query, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::Json; +use entity::{agreements::Model, Id}; +use entity_api::agreement as AgreementApi; +use service::config::ApiVersion; +use std::collections::HashMap; + +use log::*; + +/// POST create a new Agreement +#[utoipa::path( + post, + path = "/agreements", + params(ApiVersion), + request_body = entity::agreements::Model, + responses( + (status = 201, description = "Successfully Created a New Agreement", body = [entity::agreements::Model]), + (status= 422, description = "Unprocessable Entity"), + (status = 401, description = "Unauthorized"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] + +pub async fn create( + CompareApiVersion(_v): CompareApiVersion, + AuthenticatedUser(user): AuthenticatedUser, + // TODO: create a new Extractor to authorize the user to access + // the data requested + State(app_state): State, + Json(agreement_model): Json, +) -> Result { + debug!("POST Create a New Agreement from: {:?}", agreement_model); + + let agreement = AgreementApi::create(app_state.db_conn_ref(), agreement_model, user.id).await?; + + debug!("New Agreement: {:?}", agreement); + + Ok(Json(ApiResponse::new( + StatusCode::CREATED.into(), + agreement, + ))) +} + +/// GET a particular Agreement specified by its id. +#[utoipa::path( + get, + path = "/agreements/{id}", + params( + ApiVersion, + ("id" = String, Path, description = "Agreement id to retrieve") + ), + responses( + (status = 200, description = "Successfully retrieved a specific Agreement by its id", body = [entity::notes::Model]), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Agreement not found"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] +pub async fn read( + CompareApiVersion(_v): CompareApiVersion, + State(app_state): State, + Path(id): Path, +) -> Result { + debug!("GET Agreement by id: {}", id); + + let note: Option = AgreementApi::find_by_id(app_state.db_conn_ref(), id).await?; + + Ok(Json(ApiResponse::new(StatusCode::OK.into(), note))) +} + +#[utoipa::path( + put, + path = "/agreements/{id}", + params( + ApiVersion, + ("id" = Id, Path, description = "Id of agreement to update"), + ), + request_body = entity::agreements::Model, + responses( + (status = 200, description = "Successfully Updated Agreement", body = [entity::agreements::Model]), + (status = 401, description = "Unauthorized"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] +pub async fn update( + CompareApiVersion(_v): CompareApiVersion, + AuthenticatedUser(_user): AuthenticatedUser, + // TODO: create a new Extractor to authorize the user to access + // the data requested + State(app_state): State, + Path(id): Path, + Json(agreement_model): Json, +) -> Result { + debug!("PUT Update Agreement with id: {}", id); + + let agreement = AgreementApi::update(app_state.db_conn_ref(), id, agreement_model).await?; + + debug!("Updated Agreement: {:?}", agreement); + + Ok(Json(ApiResponse::new(StatusCode::OK.into(), agreement))) +} + +#[utoipa::path( + get, + path = "/agreements", + params( + ApiVersion, + ("coaching_session_id" = Option, Query, description = "Filter by coaching_session_id") + ), + responses( + (status = 200, description = "Successfully retrieved all Agreements", body = [entity::agreements::Model]), + (status = 401, description = "Unauthorized"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] +pub async fn index( + CompareApiVersion(_v): CompareApiVersion, + AuthenticatedUser(_user): AuthenticatedUser, + // TODO: create a new Extractor to authorize the user to access + // the data requested + State(app_state): State, + Query(params): Query>, +) -> Result { + debug!("GET all Agreements"); + debug!("Filter Params: {:?}", params); + + let agreements = AgreementApi::find_by(app_state.db_conn_ref(), params).await?; + + debug!("Found Agreements: {:?}", agreements); + + Ok(Json(ApiResponse::new(StatusCode::OK.into(), agreements))) +} diff --git a/web/src/controller/mod.rs b/web/src/controller/mod.rs index 642424a..72883c6 100644 --- a/web/src/controller/mod.rs +++ b/web/src/controller/mod.rs @@ -1,9 +1,12 @@ use serde::Serialize; +pub(crate) mod action_controller; +pub(crate) mod agreement_controller; pub(crate) mod coaching_session_controller; pub(crate) mod note_controller; pub(crate) mod organization; pub(crate) mod organization_controller; +pub(crate) mod overarching_goal_controller; pub(crate) mod user_session_controller; #[derive(Debug, Serialize)] diff --git a/web/src/controller/note_controller.rs b/web/src/controller/note_controller.rs index 26aab46..1fe3001 100644 --- a/web/src/controller/note_controller.rs +++ b/web/src/controller/note_controller.rs @@ -33,7 +33,7 @@ use log::*; pub async fn create( CompareApiVersion(_v): CompareApiVersion, - AuthenticatedUser(_user): AuthenticatedUser, + AuthenticatedUser(user): AuthenticatedUser, // TODO: create a new Extractor to authorize the user to access // the data requested State(app_state): State, @@ -41,7 +41,7 @@ pub async fn create( ) -> Result { debug!("POST Create a New Note from: {:?}", note_model); - let note = NoteApi::create(app_state.db_conn_ref(), note_model).await?; + let note = NoteApi::create(app_state.db_conn_ref(), note_model, user.id).await?; debug!("New Note: {:?}", note); diff --git a/web/src/controller/overarching_goal_controller.rs b/web/src/controller/overarching_goal_controller.rs new file mode 100644 index 0000000..d46c4b2 --- /dev/null +++ b/web/src/controller/overarching_goal_controller.rs @@ -0,0 +1,163 @@ +use crate::controller::ApiResponse; +use crate::extractors::{ + authenticated_user::AuthenticatedUser, compare_api_version::CompareApiVersion, +}; +use crate::{AppState, Error}; +use axum::extract::{Path, Query, State}; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::Json; +use entity::{overarching_goals::Model, Id}; +use entity_api::overarching_goal as OverarchingGoalApi; +use service::config::ApiVersion; +use std::collections::HashMap; + +use log::*; + +/// POST create a new Overarching Goal +#[utoipa::path( + post, + path = "/overarching_goals", + params(ApiVersion), + request_body = entity::overarching_goals::Model, + responses( + (status = 201, description = "Successfully Created a New Overarching Goal", body = [entity::overarching_goals::Model]), + (status= 422, description = "Unprocessable Entity"), + (status = 401, description = "Unauthorized"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] + +pub async fn create( + CompareApiVersion(_v): CompareApiVersion, + AuthenticatedUser(user): AuthenticatedUser, + // TODO: create a new Extractor to authorize the user to access + // the data requested + State(app_state): State, + Json(overarching_goals_model): Json, +) -> Result { + debug!( + "POST Create a New Overarching Goal from: {:?}", + overarching_goals_model + ); + + let overarching_goals = + OverarchingGoalApi::create(app_state.db_conn_ref(), overarching_goals_model, user.id) + .await?; + + debug!("New Overarching Goal: {:?}", overarching_goals); + + Ok(Json(ApiResponse::new( + StatusCode::CREATED.into(), + overarching_goals, + ))) +} + +/// GET a particular Overarching Goal specified by its id. +#[utoipa::path( + get, + path = "/overarching_goals/{id}", + params( + ApiVersion, + ("id" = String, Path, description = "Overarching Goal id to retrieve") + ), + responses( + (status = 200, description = "Successfully retrieved a specific Overarching Goal by its id", body = [entity::notes::Model]), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Overarching Goal not found"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] +pub async fn read( + CompareApiVersion(_v): CompareApiVersion, + State(app_state): State, + Path(id): Path, +) -> Result { + debug!("GET Overarching Goal by id: {}", id); + + let note: Option = OverarchingGoalApi::find_by_id(app_state.db_conn_ref(), id).await?; + + Ok(Json(ApiResponse::new(StatusCode::OK.into(), note))) +} + +#[utoipa::path( + put, + path = "/overarching_goals/{id}", + params( + ApiVersion, + ("id" = Id, Path, description = "Id of overarching_goals to update"), + ), + request_body = entity::overarching_goals::Model, + responses( + (status = 200, description = "Successfully Updated Overarching Goal", body = [entity::overarching_goals::Model]), + (status = 401, description = "Unauthorized"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] +pub async fn update( + CompareApiVersion(_v): CompareApiVersion, + AuthenticatedUser(_user): AuthenticatedUser, + // TODO: create a new Extractor to authorize the user to access + // the data requested + State(app_state): State, + Path(id): Path, + Json(overarching_goals_model): Json, +) -> Result { + debug!("PUT Update Overarching Goal with id: {}", id); + + let overarching_goals = + OverarchingGoalApi::update(app_state.db_conn_ref(), id, overarching_goals_model).await?; + + debug!("Updated Overarching Goal: {:?}", overarching_goals); + + Ok(Json(ApiResponse::new( + StatusCode::OK.into(), + overarching_goals, + ))) +} + +#[utoipa::path( + get, + path = "/overarching_goals", + params( + ApiVersion, + ("coaching_session_id" = Option, Query, description = "Filter by coaching_session_id") + ), + responses( + (status = 200, description = "Successfully retrieved all Overarching Goals", body = [entity::overarching_goals::Model]), + (status = 401, description = "Unauthorized"), + (status = 405, description = "Method not allowed") + ), + security( + ("cookie_auth" = []) + ) +)] +pub async fn index( + CompareApiVersion(_v): CompareApiVersion, + AuthenticatedUser(_user): AuthenticatedUser, + // TODO: create a new Extractor to authorize the user to access + // the data requested + State(app_state): State, + Query(params): Query>, +) -> Result { + debug!("GET all Overarching Goals"); + debug!("Filter Params: {:?}", params); + + let overarching_goals = OverarchingGoalApi::find_by(app_state.db_conn_ref(), params).await?; + + debug!("Found Overarching Goals: {:?}", overarching_goals); + + Ok(Json(ApiResponse::new( + StatusCode::OK.into(), + overarching_goals, + ))) +} diff --git a/web/src/router.rs b/web/src/router.rs index 5b18482..c4cc934 100644 --- a/web/src/router.rs +++ b/web/src/router.rs @@ -8,8 +8,8 @@ use entity_api::user::Backend; use tower_http::services::ServeDir; use crate::controller::{ - coaching_session_controller, note_controller, organization, organization_controller, - user_session_controller, + action_controller, agreement_controller, coaching_session_controller, note_controller, + organization, organization_controller, overarching_goal_controller, user_session_controller, }; use utoipa::{ @@ -26,6 +26,14 @@ use utoipa_rapidoc::RapiDoc; title = "Refactor Platform API" ), paths( + action_controller::create, + action_controller::update, + action_controller::index, + action_controller::read, + agreement_controller::create, + agreement_controller::update, + agreement_controller::index, + agreement_controller::read, note_controller::create, note_controller::update, note_controller::index, @@ -35,6 +43,10 @@ use utoipa_rapidoc::RapiDoc; organization_controller::create, organization_controller::update, organization_controller::delete, + overarching_goal_controller::create, + overarching_goal_controller::update, + overarching_goal_controller::index, + overarching_goal_controller::read, user_session_controller::login, user_session_controller::logout, organization::coaching_relationship_controller::index, @@ -42,11 +54,14 @@ use utoipa_rapidoc::RapiDoc; ), components( schemas( + entity::actions::Model, + entity::agreements::Model, + entity::coaching_sessions::Model, + entity::coaching_relationships::Model, entity::notes::Model, entity::organizations::Model, + entity::overarching_goals::Model, entity::users::Model, - entity::coaching_relationships::Model, - entity::coaching_sessions::Model, entity_api::user::Credentials, ) ), @@ -77,9 +92,12 @@ impl Modify for SecurityAddon { pub fn define_routes(app_state: AppState) -> Router { Router::new() + .merge(action_routes(app_state.clone())) + .merge(agreement_routes(app_state.clone())) .merge(organization_routes(app_state.clone())) .merge(note_routes(app_state.clone())) .merge(organization_coaching_relationship_routes(app_state.clone())) + .merge(overarching_goal_routes(app_state.clone())) .merge(session_routes()) .merge(protected_routes()) .merge(coaching_sessions_routes(app_state.clone())) @@ -98,6 +116,26 @@ fn organization_coaching_relationship_routes(app_state: AppState) -> Router { .with_state(app_state) } +fn action_routes(app_state: AppState) -> Router { + Router::new() + .route("/actions", post(action_controller::create)) + .route("/actions/:id", put(action_controller::update)) + .route("/actions", get(action_controller::index)) + .route("/actions/:id", get(action_controller::read)) + .route_layer(login_required!(Backend, login_url = "/login")) + .with_state(app_state) +} + +fn agreement_routes(app_state: AppState) -> Router { + Router::new() + .route("/agreements", post(agreement_controller::create)) + .route("/agreements/:id", put(agreement_controller::update)) + .route("/agreements", get(agreement_controller::index)) + .route("/agreements/:id", get(agreement_controller::read)) + .route_layer(login_required!(Backend, login_url = "/login")) + .with_state(app_state) +} + fn note_routes(app_state: AppState) -> Router { Router::new() .route("/notes", post(note_controller::create)) @@ -126,6 +164,28 @@ pub fn organization_routes(app_state: AppState) -> Router { .with_state(app_state) } +pub fn overarching_goal_routes(app_state: AppState) -> Router { + Router::new() + .route( + "/overarching_goals", + post(overarching_goal_controller::create), + ) + .route( + "/overarching_goals/:id", + put(overarching_goal_controller::update), + ) + .route( + "/overarching_goals", + get(overarching_goal_controller::index), + ) + .route( + "/overarching_goals/:id", + get(overarching_goal_controller::read), + ) + .route_layer(login_required!(Backend, login_url = "/login")) + .with_state(app_state) +} + pub fn coaching_sessions_routes(app_state: AppState) -> Router { Router::new() .route(