From b0383bdcba7413de1209d23adceb653fa1d46919 Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Thu, 1 Aug 2024 07:31:43 -0400 Subject: [PATCH 01/10] add index, read, update for agreements --- entity/src/agreements.rs | 7 +- entity_api/src/agreement.rs | 167 +++++++++++++++++++++ entity_api/src/lib.rs | 1 + web/src/controller/agreement_controller.rs | 121 +++++++++++++++ web/src/controller/mod.rs | 1 + web/src/router.rs | 15 +- 6 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 entity_api/src/agreement.rs create mode 100644 web/src/controller/agreement_controller.rs diff --git a/entity/src/agreements.rs b/entity/src/agreements.rs index bbd1f647..d9a5f6d1 100644 --- a/entity/src/agreements.rs +++ b/entity/src/agreements.rs @@ -3,17 +3,22 @@ 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)] +#[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 user_id: Id, + #[serde(skip_deserializing)] pub created_at: DateTimeWithTimeZone, + #[serde(skip_deserializing)] pub updated_at: DateTimeWithTimeZone, } diff --git a/entity_api/src/agreement.rs b/entity_api/src/agreement.rs new file mode 100644 index 00000000..d1ef2771 --- /dev/null +++ b/entity_api/src/agreement.rs @@ -0,0 +1,167 @@ +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) -> 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), + details: Set(agreement_model.details), + user_id: Set(agreement_model.user_id), + created_at: Set(now.into()), + updated_at: Set(now.into()), + ..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), + details: Set(model.details), + user_id: Unchanged(agreement.user_id), + 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( + 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(), + coaching_session_id: Id::new_v4(), + details: Some("This is a agreement".to_owned()), + user_id: Id::new_v4(), + 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()).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(), + details: Some("This is a agreement".to_owned()), + user_id: Id::new_v4(), + 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.details, agreement_model.details); + + 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"."details", "agreements"."user_id", "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 9fd6877e..a131f56e 100644 --- a/entity_api/src/lib.rs +++ b/entity_api/src/lib.rs @@ -4,6 +4,7 @@ use sea_orm::{ActiveModelTrait, DatabaseConnection, Set}; use entity::{coaching_relationships, coaching_sessions, organizations, users, Id}; +pub mod agreement; pub mod coaching_relationship; pub mod coaching_session; pub mod error; diff --git a/web/src/controller/agreement_controller.rs b/web/src/controller/agreement_controller.rs new file mode 100644 index 00000000..4af27946 --- /dev/null +++ b/web/src/controller/agreement_controller.rs @@ -0,0 +1,121 @@ +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).await?; + + debug!("New Agreement: {:?}", agreement); + + Ok(Json(ApiResponse::new(StatusCode::CREATED.into(), agreement))) +} + +#[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 642424a1..025406f2 100644 --- a/web/src/controller/mod.rs +++ b/web/src/controller/mod.rs @@ -1,5 +1,6 @@ use serde::Serialize; +pub(crate) mod agreement_controller; pub(crate) mod coaching_session_controller; pub(crate) mod note_controller; pub(crate) mod organization; diff --git a/web/src/router.rs b/web/src/router.rs index 5b184828..ab5369a4 100644 --- a/web/src/router.rs +++ b/web/src/router.rs @@ -8,7 +8,7 @@ use entity_api::user::Backend; use tower_http::services::ServeDir; use crate::controller::{ - coaching_session_controller, note_controller, organization, organization_controller, + agreement_controller, coaching_session_controller, note_controller, organization, organization_controller, user_session_controller, }; @@ -26,6 +26,9 @@ use utoipa_rapidoc::RapiDoc; title = "Refactor Platform API" ), paths( + agreement_controller::create, + agreement_controller::update, + agreement_controller::index, note_controller::create, note_controller::update, note_controller::index, @@ -77,6 +80,7 @@ impl Modify for SecurityAddon { pub fn define_routes(app_state: AppState) -> Router { Router::new() + .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())) @@ -98,6 +102,15 @@ fn organization_coaching_relationship_routes(app_state: AppState) -> Router { .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_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)) From 584fdab80e54fb6da568a82962f55f6a49f56182 Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Sat, 3 Aug 2024 15:59:52 -0400 Subject: [PATCH 02/10] rebase and add agreement by id --- entity_api/src/agreement.rs | 53 +++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/entity_api/src/agreement.rs b/entity_api/src/agreement.rs index d1ef2771..a445b12a 100644 --- a/entity_api/src/agreement.rs +++ b/entity_api/src/agreement.rs @@ -57,6 +57,32 @@ pub async fn update(db: &DatabaseConnection, id: Id, model: Model) -> Result 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, @@ -130,7 +156,10 @@ mod tests { }; let db = MockDatabase::new(DatabaseBackend::Postgres) - .append_query_results(vec![vec![agreement_model.clone()], vec![agreement_model.clone()]]) + .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?; @@ -141,7 +170,27 @@ mod tests { } #[tokio::test] - async fn find_by_returns_all_agreements_associated_with_coaching_session() -> Result<(), Error> { + 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"."details", "agreements"."user_id", "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(); From 6644d5e664ead9e262c8fb6bb5b178fa753c0c2d Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Mon, 5 Aug 2024 15:26:08 -0500 Subject: [PATCH 03/10] Add agreements::Model schema so RAPIdoc can display it properly --- web/src/router.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/router.rs b/web/src/router.rs index ab5369a4..b18c6f9f 100644 --- a/web/src/router.rs +++ b/web/src/router.rs @@ -45,6 +45,7 @@ use utoipa_rapidoc::RapiDoc; ), components( schemas( + entity::agreements::Model, entity::notes::Model, entity::organizations::Model, entity::users::Model, From b625f0871cb71b9a5edba7aad270854d239d6d96 Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Fri, 9 Aug 2024 08:43:54 -0400 Subject: [PATCH 04/10] add status and status_changed_at --- docs/db/refactor_platform_rs.dbml | 12 +++++-- entity/src/actions.rs | 4 ++- entity/src/agreements.rs | 6 +++- entity/src/lib.rs | 1 + entity/src/status.rs | 19 ++++++++++ entity_api/src/agreement.rs | 3 ++ migration/src/refactor_platform_rs.sql | 14 ++++++-- web/src/controller/agreement_controller.rs | 40 +++++++++++++++++++--- web/src/router.rs | 6 ++-- 9 files changed, 91 insertions(+), 14 deletions(-) create mode 100644 entity/src/status.rs diff --git a/docs/db/refactor_platform_rs.dbml b/docs/db/refactor_platform_rs.dbml index a2bfda90..196043ba 100644 --- a/docs/db/refactor_platform_rs.dbml +++ b/docs/db/refactor_platform_rs.dbml @@ -66,6 +66,8 @@ Table refactor_platform.agreements { 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'] 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'] } @@ -77,12 +79,18 @@ Table refactor_platform.actions { // its due_by is passed or it was completed by the coachee coaching_session_id uuid [not null] 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/src/actions.rs b/entity/src/actions.rs index 123a8c8a..7660baf3 100644 --- a/entity/src/actions.rs +++ b/entity/src/actions.rs @@ -1,6 +1,6 @@ //! `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}; @@ -13,6 +13,8 @@ pub struct Model { pub due_by: Option, pub completed: Option, pub completed_at: Option, + pub status: status::Status, + pub status_changed_at: Option, pub created_at: DateTimeWithTimeZone, pub updated_at: DateTimeWithTimeZone, } diff --git a/entity/src/agreements.rs b/entity/src/agreements.rs index d9a5f6d1..01567f08 100644 --- a/entity/src/agreements.rs +++ b/entity/src/agreements.rs @@ -1,6 +1,6 @@ //! `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; @@ -17,6 +17,10 @@ pub struct Model { pub details: Option, 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 49eacb13..d924289f 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/status.rs b/entity/src/status.rs new file mode 100644 index 00000000..ac1eef3d --- /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/agreement.rs b/entity_api/src/agreement.rs index a445b12a..45d0ea5f 100644 --- a/entity_api/src/agreement.rs +++ b/entity_api/src/agreement.rs @@ -22,6 +22,7 @@ pub async fn create(db: &DatabaseConnection, agreement_model: Model) -> Result Result, + 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( @@ -114,8 +147,5 @@ pub async fn index( debug!("Found Agreements: {:?}", agreements); - Ok(Json(ApiResponse::new( - StatusCode::OK.into(), - agreements, - ))) + Ok(Json(ApiResponse::new(StatusCode::OK.into(), agreements))) } diff --git a/web/src/router.rs b/web/src/router.rs index b18c6f9f..f2126418 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::{ - agreement_controller, coaching_session_controller, note_controller, organization, organization_controller, - user_session_controller, + agreement_controller, coaching_session_controller, note_controller, organization, + organization_controller, user_session_controller, }; use utoipa::{ @@ -29,6 +29,7 @@ use utoipa_rapidoc::RapiDoc; agreement_controller::create, agreement_controller::update, agreement_controller::index, + agreement_controller::read, note_controller::create, note_controller::update, note_controller::index, @@ -108,6 +109,7 @@ fn agreement_routes(app_state: AppState) -> Router { .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) } From acced67aa44d350ef5fe0cd0d486bdbbdef01ddc Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Wed, 14 Aug 2024 09:53:05 -0400 Subject: [PATCH 05/10] update actions and agreements --- docs/db/refactor_platform_rs.dbml | 7 +- entity/src/actions.rs | 26 ++- entity/src/agreements.rs | 2 +- entity/src/overarching_goals.rs | 17 +- entity_api/src/action.rs | 202 ++++++++++++++++++++++++ entity_api/src/agreement.rs | 18 ++- entity_api/src/lib.rs | 1 + migration/src/refactor_platform_rs.sql | 19 ++- web/src/controller/action_controller.rs | 148 +++++++++++++++++ web/src/controller/mod.rs | 1 + web/src/router.rs | 20 ++- 11 files changed, 440 insertions(+), 21 deletions(-) create mode 100644 entity_api/src/action.rs create mode 100644 web/src/controller/action_controller.rs diff --git a/docs/db/refactor_platform_rs.dbml b/docs/db/refactor_platform_rs.dbml index 196043ba..7cb04e4e 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,7 +65,7 @@ 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 @@ -78,6 +79,8 @@ 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 status status [not null] status_changed_at timestamptz diff --git a/entity/src/actions.rs b/entity/src/actions.rs index 7660baf3..11b56a7c 100644 --- a/entity/src/actions.rs +++ b/entity/src/actions.rs @@ -3,19 +3,25 @@ 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, + 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, } @@ -29,6 +35,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 { @@ -37,4 +51,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 01567f08..fdf40732 100644 --- a/entity/src/agreements.rs +++ b/entity/src/agreements.rs @@ -14,7 +14,7 @@ pub struct Model { pub id: Id, #[sea_orm(unique)] pub coaching_session_id: Id, - pub details: Option, + pub body: Option, pub user_id: Id, #[serde(skip_deserializing)] pub status: status::Status, diff --git a/entity/src/overarching_goals.rs b/entity/src/overarching_goals.rs index 6bbb5177..5e2059fe 100644 --- a/entity/src/overarching_goals.rs +++ b/entity/src/overarching_goals.rs @@ -10,8 +10,9 @@ pub struct Model { #[sea_orm(primary_key)] pub id: Id, pub coaching_session_id: Option, + pub user_id: Id, pub title: Option, - pub details: Option, + pub body: Option, pub completed_at: Option, pub created_at: DateTimeWithTimeZone, pub updated_at: DateTimeWithTimeZone, @@ -27,6 +28,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 +44,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/entity_api/src/action.rs b/entity_api/src/action.rs new file mode 100644 index 00000000..643ea526 --- /dev/null +++ b/entity_api/src/action.rs @@ -0,0 +1,202 @@ +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) -> 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(action_model.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(), + coaching_session_id: Id::new_v4(), + body: Some("This is a action".to_owned()), + due_by: Some(now.into()), + 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()]]) + .into_connection(); + + let action = create(&db, action_model.clone().into()).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 index 45d0ea5f..8f09353c 100644 --- a/entity_api/src/agreement.rs +++ b/entity_api/src/agreement.rs @@ -18,7 +18,7 @@ pub async fn create(db: &DatabaseConnection, agreement_model: Model) -> Result Result, + 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).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 = "Note 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/mod.rs b/web/src/controller/mod.rs index 025406f2..c75be33d 100644 --- a/web/src/controller/mod.rs +++ b/web/src/controller/mod.rs @@ -1,5 +1,6 @@ use serde::Serialize; +pub(crate) mod action_controller; pub(crate) mod agreement_controller; pub(crate) mod coaching_session_controller; pub(crate) mod note_controller; diff --git a/web/src/router.rs b/web/src/router.rs index f2126418..7c53be3e 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::{ - agreement_controller, coaching_session_controller, note_controller, organization, - organization_controller, user_session_controller, + action_controller, agreement_controller, coaching_session_controller, note_controller, + organization, organization_controller, user_session_controller, }; use utoipa::{ @@ -26,6 +26,10 @@ 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, @@ -46,6 +50,7 @@ use utoipa_rapidoc::RapiDoc; ), components( schemas( + entity::actions::Model, entity::agreements::Model, entity::notes::Model, entity::organizations::Model, @@ -82,6 +87,7 @@ 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())) @@ -104,6 +110,16 @@ 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)) From b7527aed2637615f5664cbe9937384aac2520534 Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Thu, 15 Aug 2024 09:46:09 -0400 Subject: [PATCH 06/10] add overarching goals endpoints --- entity/src/overarching_goals.rs | 7 +- entity_api/src/lib.rs | 1 + entity_api/src/overarching_goal.rs | 227 ++++++++++++++++++ web/src/controller/mod.rs | 1 + .../controller/overarching_goal_controller.rs | 162 +++++++++++++ web/src/router.rs | 34 ++- 6 files changed, 428 insertions(+), 4 deletions(-) create mode 100644 entity_api/src/overarching_goal.rs create mode 100644 web/src/controller/overarching_goal_controller.rs diff --git a/entity/src/overarching_goals.rs b/entity/src/overarching_goals.rs index 5e2059fe..229d1486 100644 --- a/entity/src/overarching_goals.rs +++ b/entity/src/overarching_goals.rs @@ -3,18 +3,23 @@ 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, pub user_id: Id, pub title: 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, } diff --git a/entity_api/src/lib.rs b/entity_api/src/lib.rs index 1d634ca9..19d4b93a 100644 --- a/entity_api/src/lib.rs +++ b/entity_api/src/lib.rs @@ -11,6 +11,7 @@ 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/overarching_goal.rs b/entity_api/src/overarching_goal.rs new file mode 100644 index 00000000..9dd192d1 --- /dev/null +++ b/entity_api/src/overarching_goal.rs @@ -0,0 +1,227 @@ +use super::error::{EntityApiErrorCode, Error}; +use crate::uuid_parse_str; +use entity::overarching_goals::{self, ActiveModel, Entity, Model}; +use entity::Id; +use sea_orm::{ + entity::prelude::*, + ActiveModelTrait, + ActiveValue::{Set, Unchanged}, + DatabaseConnection, TryIntoModel, +}; +use std::collections::HashMap; + +use log::*; + +pub async fn create( + db: &DatabaseConnection, + overarching_goal_model: Model, +) -> 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(overarching_goal_model.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, Transoverarching_goal}; + + #[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(), + coaching_session_id: Id::new_v4(), + body: Some("This is a overarching_goal".to_owned()), + due_by: Some(now.into()), + 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![overarching_goal_model.clone()]]) + .into_connection(); + + let overarching_goal = create(&db, overarching_goal_model.clone().into()).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: Id::new_v4(), + due_by: Some(now.into()), + body: Some("This is a overarching_goal".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![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(), + [Transoverarching_goal::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "overarching_goals"."id", "overarching_goals"."coaching_session_id", "overarching_goals"."user_id", "overarching_goals"."body", "overarching_goals"."title", "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/web/src/controller/mod.rs b/web/src/controller/mod.rs index c75be33d..72883c69 100644 --- a/web/src/controller/mod.rs +++ b/web/src/controller/mod.rs @@ -6,6 +6,7 @@ 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/overarching_goal_controller.rs b/web/src/controller/overarching_goal_controller.rs new file mode 100644 index 00000000..4ee3833c --- /dev/null +++ b/web/src/controller/overarching_goal_controller.rs @@ -0,0 +1,162 @@ +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 OverarchingGoal", 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).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 = "Note 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 OverarchingGoals"); + 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 7c53be3e..c4cc934f 100644 --- a/web/src/router.rs +++ b/web/src/router.rs @@ -9,7 +9,7 @@ use tower_http::services::ServeDir; use crate::controller::{ action_controller, agreement_controller, coaching_session_controller, note_controller, - organization, organization_controller, user_session_controller, + organization, organization_controller, overarching_goal_controller, user_session_controller, }; use utoipa::{ @@ -43,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, @@ -52,11 +56,12 @@ use utoipa_rapidoc::RapiDoc; 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, ) ), @@ -92,6 +97,7 @@ pub fn define_routes(app_state: AppState) -> Router { .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())) @@ -158,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( From 6274b0c0347ec9fe8ef8d9b144b270062a0bd591 Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Wed, 21 Aug 2024 08:38:57 -0400 Subject: [PATCH 07/10] pull user_id from user sessions --- entity/src/actions.rs | 1 + entity/src/agreements.rs | 1 + entity/src/notes.rs | 1 + entity/src/overarching_goals.rs | 1 + entity_api/src/action.rs | 12 +++++--- entity_api/src/agreement.rs | 12 +++++--- entity_api/src/note.rs | 12 +++++--- entity_api/src/overarching_goal.rs | 28 +++++++++---------- web/src/controller/action_controller.rs | 4 +-- web/src/controller/agreement_controller.rs | 4 +-- web/src/controller/note_controller.rs | 4 +-- .../controller/overarching_goal_controller.rs | 5 ++-- 12 files changed, 51 insertions(+), 34 deletions(-) diff --git a/entity/src/actions.rs b/entity/src/actions.rs index 11b56a7c..8fb18ac1 100644 --- a/entity/src/actions.rs +++ b/entity/src/actions.rs @@ -12,6 +12,7 @@ pub struct Model { #[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, diff --git a/entity/src/agreements.rs b/entity/src/agreements.rs index fdf40732..7785b640 100644 --- a/entity/src/agreements.rs +++ b/entity/src/agreements.rs @@ -15,6 +15,7 @@ pub struct Model { #[sea_orm(unique)] pub coaching_session_id: Id, pub body: Option, + #[serde(skip_deserializing)] pub user_id: Id, #[serde(skip_deserializing)] pub status: status::Status, diff --git a/entity/src/notes.rs b/entity/src/notes.rs index fc40d183..3f44534f 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 229d1486..85dd4718 100644 --- a/entity/src/overarching_goals.rs +++ b/entity/src/overarching_goals.rs @@ -12,6 +12,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: Id, pub coaching_session_id: Option, + #[serde(skip_deserializing)] pub user_id: Id, pub title: Option, pub body: Option, diff --git a/entity_api/src/action.rs b/entity_api/src/action.rs index 643ea526..066c78f6 100644 --- a/entity_api/src/action.rs +++ b/entity_api/src/action.rs @@ -11,14 +11,18 @@ use std::collections::HashMap; use log::*; -pub async fn create(db: &DatabaseConnection, action_model: Model) -> Result { +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(action_model.user_id), + user_id: Set(user_id), due_by: Set(action_model.due_by), body: Set(action_model.body), created_at: Set(now.into()), @@ -127,10 +131,10 @@ mod tests { 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()), - user_id: Id::new_v4(), status_changed_at: None, status: Default::default(), created_at: now.into(), @@ -141,7 +145,7 @@ mod tests { .append_query_results(vec![vec![action_model.clone()]]) .into_connection(); - let action = create(&db, action_model.clone().into()).await?; + let action = create(&db, action_model.clone().into(), Id::new_v4()).await?; assert_eq!(action.id, action_model.id); diff --git a/entity_api/src/agreement.rs b/entity_api/src/agreement.rs index 8f09353c..07467187 100644 --- a/entity_api/src/agreement.rs +++ b/entity_api/src/agreement.rs @@ -11,7 +11,11 @@ use std::collections::HashMap; use log::*; -pub async fn create(db: &DatabaseConnection, agreement_model: Model) -> Result { +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(); @@ -19,7 +23,7 @@ pub async fn create(db: &DatabaseConnection, agreement_model: Model) -> Result 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: {:?}", @@ -25,7 +26,7 @@ pub async fn create( let overarching_goal_active_model: ActiveModel = ActiveModel { coaching_session_id: Set(overarching_goal_model.coaching_session_id), - user_id: Set(overarching_goal_model.user_id), + user_id: Set(user_id), title: Set(overarching_goal_model.title), body: Set(overarching_goal_model.body), created_at: Set(now.into()), @@ -135,7 +136,7 @@ pub async fn find_by( mod tests { use super::*; use entity::{overarching_goals::Model, Id}; - use sea_orm::{DatabaseBackend, MockDatabase, Transoverarching_goal}; + use sea_orm::{DatabaseBackend, MockDatabase, Transaction}; #[tokio::test] async fn create_returns_a_new_overarching_goal_model() -> Result<(), Error> { @@ -143,12 +144,11 @@ mod tests { let overarching_goal_model = Model { id: Id::new_v4(), - coaching_session_id: Id::new_v4(), - body: Some("This is a overarching_goal".to_owned()), - due_by: Some(now.into()), user_id: Id::new_v4(), - status_changed_at: None, - status: Default::default(), + 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(), }; @@ -157,7 +157,8 @@ mod tests { .append_query_results(vec![vec![overarching_goal_model.clone()]]) .into_connection(); - let overarching_goal = create(&db, overarching_goal_model.clone().into()).await?; + let overarching_goal = + create(&db, overarching_goal_model.clone().into(), Id::new_v4()).await?; assert_eq!(overarching_goal.id, overarching_goal_model.id); @@ -170,12 +171,11 @@ mod tests { let overarching_goal_model = Model { id: Id::new_v4(), - coaching_session_id: Id::new_v4(), - due_by: Some(now.into()), + 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(), - status_changed_at: None, - status: Default::default(), + completed_at: Some(now.into()), created_at: now.into(), updated_at: now.into(), }; @@ -215,9 +215,9 @@ mod tests { assert_eq!( db.into_transaction_log(), - [Transoverarching_goal::from_sql_and_values( + [Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT "overarching_goals"."id", "overarching_goals"."coaching_session_id", "overarching_goals"."user_id", "overarching_goals"."body", "overarching_goals"."title", "overarching_goals"."created_at", "overarching_goals"."updated_at" FROM "refactor_platform"."overarching_goals" WHERE "overarching_goals"."coaching_session_id" = $1"#, + 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()] )] ); diff --git a/web/src/controller/action_controller.rs b/web/src/controller/action_controller.rs index d831d1d4..159bc5bb 100644 --- a/web/src/controller/action_controller.rs +++ b/web/src/controller/action_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 Action from: {:?}", action_model); - let action = ActionApi::create(app_state.db_conn_ref(), action_model).await?; + let action = ActionApi::create(app_state.db_conn_ref(), action_model, user.id).await?; debug!("New Action: {:?}", action); diff --git a/web/src/controller/agreement_controller.rs b/web/src/controller/agreement_controller.rs index cca41cbe..7f6a9561 100644 --- a/web/src/controller/agreement_controller.rs +++ b/web/src/controller/agreement_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 Agreement from: {:?}", agreement_model); - let agreement = AgreementApi::create(app_state.db_conn_ref(), agreement_model).await?; + let agreement = AgreementApi::create(app_state.db_conn_ref(), agreement_model, user.id).await?; debug!("New Agreement: {:?}", agreement); diff --git a/web/src/controller/note_controller.rs b/web/src/controller/note_controller.rs index 26aab46a..1fe30014 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 index 4ee3833c..f4486797 100644 --- a/web/src/controller/overarching_goal_controller.rs +++ b/web/src/controller/overarching_goal_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, @@ -45,7 +45,8 @@ pub async fn create( ); let overarching_goals = - OverarchingGoalApi::create(app_state.db_conn_ref(), overarching_goals_model).await?; + OverarchingGoalApi::create(app_state.db_conn_ref(), overarching_goals_model, user.id) + .await?; debug!("New Overarching Goal: {:?}", overarching_goals); From e52246ec03ec43c2618167139f5b5cea63413266 Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Fri, 23 Aug 2024 13:56:08 -0400 Subject: [PATCH 08/10] Update README.md with new DBML PNG --- entity/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/entity/README.md b/entity/README.md index a1af1a56..4e6fdf2f 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 From a91bd6bd5b279a35173b310a2cf46566d58ad9fb Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Fri, 23 Aug 2024 13:57:13 -0400 Subject: [PATCH 09/10] Update web/src/controller/agreement_controller.rs Co-authored-by: Jim Hodapp --- web/src/controller/agreement_controller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/controller/agreement_controller.rs b/web/src/controller/agreement_controller.rs index 7f6a9561..db70a1ff 100644 --- a/web/src/controller/agreement_controller.rs +++ b/web/src/controller/agreement_controller.rs @@ -62,7 +62,7 @@ pub async fn create( responses( (status = 200, description = "Successfully retrieved a specific Agreement by its id", body = [entity::notes::Model]), (status = 401, description = "Unauthorized"), - (status = 404, description = "Note not found"), + (status = 404, description = "Agreement not found"), (status = 405, description = "Method not allowed") ), security( From 2966a9b44b6f62923555024d3b7be14e69804a1f Mon Sep 17 00:00:00 2001 From: Caleb Bourg Date: Fri, 23 Aug 2024 13:58:39 -0400 Subject: [PATCH 10/10] Review Suggestions Co-authored-by: Jim Hodapp --- web/src/controller/action_controller.rs | 2 +- web/src/controller/overarching_goal_controller.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/controller/action_controller.rs b/web/src/controller/action_controller.rs index 159bc5bb..9448d4bb 100644 --- a/web/src/controller/action_controller.rs +++ b/web/src/controller/action_controller.rs @@ -59,7 +59,7 @@ pub async fn create( responses( (status = 200, description = "Successfully retrieved a specific Action by its id", body = [entity::notes::Model]), (status = 401, description = "Unauthorized"), - (status = 404, description = "Note not found"), + (status = 404, description = "Action not found"), (status = 405, description = "Method not allowed") ), security( diff --git a/web/src/controller/overarching_goal_controller.rs b/web/src/controller/overarching_goal_controller.rs index f4486797..d46c4b29 100644 --- a/web/src/controller/overarching_goal_controller.rs +++ b/web/src/controller/overarching_goal_controller.rs @@ -21,7 +21,7 @@ use log::*; params(ApiVersion), request_body = entity::overarching_goals::Model, responses( - (status = 201, description = "Successfully Created a New OverarchingGoal", body = [entity::overarching_goals::Model]), + (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") @@ -67,7 +67,7 @@ pub async fn create( responses( (status = 200, description = "Successfully retrieved a specific Overarching Goal by its id", body = [entity::notes::Model]), (status = 401, description = "Unauthorized"), - (status = 404, description = "Note not found"), + (status = 404, description = "Overarching Goal not found"), (status = 405, description = "Method not allowed") ), security( @@ -149,7 +149,7 @@ pub async fn index( State(app_state): State, Query(params): Query>, ) -> Result { - debug!("GET all OverarchingGoals"); + debug!("GET all Overarching Goals"); debug!("Filter Params: {:?}", params); let overarching_goals = OverarchingGoalApi::find_by(app_state.db_conn_ref(), params).await?;