From 8467e13b951da0d3e8d1c0d4626f601a700b1d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Greinhofer?= Date: Thu, 28 Mar 2024 15:20:48 -0500 Subject: [PATCH] Add endpoint (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds endpoints to: - get the city census details - get partial/full bna results - post a new bna result Signed-off-by: Rémy Greinhofer --- .github/workflows/deployment-staging.yml | 2 + entity/src/wrappers/bna.rs | 71 +++++ entity/src/wrappers/brokenspoke_pipeline.rs | 75 +++++ entity/src/wrappers/census.rs | 44 +++ entity/src/wrappers/city.rs | 67 ++++ entity/src/wrappers/mod.rs | 286 +----------------- entity/src/wrappers/submission.rs | 208 +++++++++++++ examples/query.rs | 22 +- examples/seeder.rs | 11 +- lambdas/Cargo.toml | 25 +- lambdas/requests.rest | 4 + .../src/bnas-analysis/get-bnas-analysis.rs | 6 +- .../src/bnas-analysis/patch-bnas-analysis.rs | 7 +- .../src/bnas-analysis/post-bnas-analysis.rs | 4 +- lambdas/src/bnas/get-bnas.rs | 263 +++++++++++++++- lambdas/src/bnas/patch-bnas.rs | 102 +++++++ lambdas/src/bnas/post-bnas.rs | 107 +++++++ .../get-cities-submissions.rs | 7 +- .../patch-cities-submissions.rs | 4 +- .../post-cities-submissions.rs | 4 +- lambdas/src/cities/get-cities-bnas.rs | 7 +- lambdas/src/cities/get-cities-census.rs | 87 ++++++ lambdas/src/cities/get-cities.rs | 6 +- lambdas/src/lib.rs | 2 +- 24 files changed, 1090 insertions(+), 331 deletions(-) create mode 100644 entity/src/wrappers/bna.rs create mode 100644 entity/src/wrappers/brokenspoke_pipeline.rs create mode 100644 entity/src/wrappers/census.rs create mode 100644 entity/src/wrappers/city.rs create mode 100644 entity/src/wrappers/submission.rs create mode 100644 lambdas/src/bnas/patch-bnas.rs create mode 100644 lambdas/src/bnas/post-bnas.rs create mode 100644 lambdas/src/cities/get-cities-census.rs diff --git a/.github/workflows/deployment-staging.yml b/.github/workflows/deployment-staging.yml index c6d5732..f3deb01 100644 --- a/.github/workflows/deployment-staging.yml +++ b/.github/workflows/deployment-staging.yml @@ -41,9 +41,11 @@ jobs: get-bnas-results get-cities get-cities-bnas + get-cities-census get-cities-submissions patch-bnas-analysis patch-cities-submissions + post-bnas post-bnas-analysis post-bnas-enqueue post-cities-submissions" diff --git a/entity/src/wrappers/bna.rs b/entity/src/wrappers/bna.rs new file mode 100644 index 0000000..39ddea3 --- /dev/null +++ b/entity/src/wrappers/bna.rs @@ -0,0 +1,71 @@ +use sea_orm::prelude::Uuid; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BNASummary { + pub bna_uuid: Uuid, + pub city_id: Uuid, + pub score: f64, + pub version: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BNAInfrastructure { + pub low_stress_miles: Option, + pub high_stress_miles: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BNARecreation { + pub community_centers: Option, + pub parks: Option, + pub recreation_trails: Option, + pub score: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BNAOpportunity { + pub employment: Option, + pub higher_education: Option, + pub k12_education: Option, + pub score: Option, + pub technical_vocational_college: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BNACoreServices { + pub dentists: Option, + pub doctors: Option, + pub grocery: Option, + pub hospitals: Option, + pub pharmacies: Option, + pub score: Option, + pub social_services: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BNAFeatures { + pub people: Option, + pub retail: Option, + pub transit: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BNAPost { + pub core_services: BNACoreServices, + pub features: BNAFeatures, + pub infrastructure: BNAInfrastructure, + pub opportunity: BNAOpportunity, + pub recreation: BNARecreation, + pub summary: BNASummary, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BNAPatch { + pub core_services: BNACoreServices, + pub features: BNAFeatures, + pub infrastructure: BNAInfrastructure, + pub opportunity: BNAOpportunity, + pub recreation: BNARecreation, + pub summary: BNASummary, +} diff --git a/entity/src/wrappers/brokenspoke_pipeline.rs b/entity/src/wrappers/brokenspoke_pipeline.rs new file mode 100644 index 0000000..2eead83 --- /dev/null +++ b/entity/src/wrappers/brokenspoke_pipeline.rs @@ -0,0 +1,75 @@ +use crate::entities::{brokenspoke_pipeline, sea_orm_active_enums}; +use sea_orm::{ + prelude::{Json, TimeDateTimeWithTimeZone, Uuid}, + ActiveValue, IntoActiveModel, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BrokenspokePipelinePost { + pub state: Option, + pub state_machine_id: Uuid, + pub scheduled_trigger_id: Option, + pub sqs_message: Option, + pub neon_branch_id: Option, + pub fargate_task_arn: Option, + pub s3_bucket: Option, + pub start_time: TimeDateTimeWithTimeZone, + pub end_time: Option, + pub torn_down: Option, +} + +impl IntoActiveModel for BrokenspokePipelinePost { + fn into_active_model(self) -> brokenspoke_pipeline::ActiveModel { + brokenspoke_pipeline::ActiveModel { + state: ActiveValue::Set(self.state), + state_machine_id: ActiveValue::Set(self.state_machine_id), + sqs_message: ActiveValue::Set(self.sqs_message), + neon_branch_id: ActiveValue::Set(self.neon_branch_id), + fargate_task_arn: ActiveValue::Set(self.fargate_task_arn), + s3_bucket: ActiveValue::Set(self.s3_bucket), + scheduled_trigger_id: ActiveValue::Set(self.scheduled_trigger_id), + start_time: ActiveValue::Set(self.start_time), + end_time: ActiveValue::Set(self.end_time), + torn_down: ActiveValue::Set(self.torn_down), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct BrokenspokePipelinePatch { + pub state: Option>, + pub scheduled_trigger_id: Option>, + pub sqs_message: Option>, + pub neon_branch_id: Option>, + pub fargate_task_arn: Option>, + pub s3_bucket: Option>, + pub start_time: Option>, + pub end_time: Option>, + pub torn_down: Option>, +} + +impl IntoActiveModel for BrokenspokePipelinePatch { + fn into_active_model(self) -> brokenspoke_pipeline::ActiveModel { + brokenspoke_pipeline::ActiveModel { + state_machine_id: ActiveValue::NotSet, + state: self.state.map_or(ActiveValue::NotSet, ActiveValue::Set), + sqs_message: self + .sqs_message + .map_or(ActiveValue::NotSet, ActiveValue::Set), + neon_branch_id: self + .neon_branch_id + .map_or(ActiveValue::NotSet, ActiveValue::Set), + fargate_task_arn: self + .fargate_task_arn + .map_or(ActiveValue::NotSet, ActiveValue::Set), + s3_bucket: self.s3_bucket.map_or(ActiveValue::NotSet, ActiveValue::Set), + scheduled_trigger_id: self + .scheduled_trigger_id + .map_or(ActiveValue::NotSet, ActiveValue::Set), + start_time: ActiveValue::NotSet, + end_time: self.end_time.map_or(ActiveValue::NotSet, ActiveValue::Set), + torn_down: self.torn_down.map_or(ActiveValue::NotSet, ActiveValue::Set), + } + } +} diff --git a/entity/src/wrappers/census.rs b/entity/src/wrappers/census.rs new file mode 100644 index 0000000..cf72d4e --- /dev/null +++ b/entity/src/wrappers/census.rs @@ -0,0 +1,44 @@ +use crate::census; +use sea_orm::{prelude::Uuid, ActiveValue, IntoActiveModel}; + +pub struct CensusPost { + pub city_id: Uuid, + pub fips_code: String, + pub pop_size: i32, + pub population: i32, +} + +impl IntoActiveModel for CensusPost { + fn into_active_model(self) -> census::ActiveModel { + census::ActiveModel { + census_id: ActiveValue::NotSet, + city_id: ActiveValue::Set(self.city_id), + created_at: ActiveValue::NotSet, + fips_code: ActiveValue::Set(self.fips_code), + pop_size: ActiveValue::Set(self.pop_size), + population: ActiveValue::Set(self.population), + } + } +} + +pub struct CensusPatch { + pub city_id: Option, + pub fips_code: Option, + pub pop_size: Option, + pub population: Option, +} + +impl IntoActiveModel for CensusPatch { + fn into_active_model(self) -> census::ActiveModel { + census::ActiveModel { + census_id: ActiveValue::NotSet, + city_id: self.city_id.map_or(ActiveValue::NotSet, ActiveValue::Set), + created_at: ActiveValue::NotSet, + fips_code: self.fips_code.map_or(ActiveValue::NotSet, ActiveValue::Set), + pop_size: self.pop_size.map_or(ActiveValue::NotSet, ActiveValue::Set), + population: self + .population + .map_or(ActiveValue::NotSet, ActiveValue::Set), + } + } +} diff --git a/entity/src/wrappers/city.rs b/entity/src/wrappers/city.rs new file mode 100644 index 0000000..151cf93 --- /dev/null +++ b/entity/src/wrappers/city.rs @@ -0,0 +1,67 @@ +use crate::city; +use sea_orm::{ActiveValue, IntoActiveModel}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CityPost { + pub country: String, + pub latitude: f64, + pub longitude: f64, + pub name: String, + pub region: String, + pub state: String, + pub state_abbrev: Option, + pub speed_limit: Option, +} + +impl IntoActiveModel for CityPost { + fn into_active_model(self) -> city::ActiveModel { + city::ActiveModel { + city_id: ActiveValue::NotSet, + country: ActiveValue::Set(self.country), + latitude: ActiveValue::Set(self.latitude), + longitude: ActiveValue::Set(self.longitude), + name: ActiveValue::Set(self.name), + region: ActiveValue::Set(self.region), + state: ActiveValue::Set(self.state), + state_abbrev: ActiveValue::Set(self.state_abbrev), + speed_limit: ActiveValue::Set(self.speed_limit), + created_at: ActiveValue::NotSet, + updated_at: ActiveValue::NotSet, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct CityPatch { + pub country: Option, + pub latitude: Option, + pub longitude: Option, + pub name: Option, + pub region: Option, + pub state: Option, + pub state_abbrev: Option>, + pub speed_limit: Option>, +} + +impl IntoActiveModel for CityPatch { + fn into_active_model(self) -> city::ActiveModel { + city::ActiveModel { + city_id: ActiveValue::NotSet, + country: self.country.map_or(ActiveValue::NotSet, ActiveValue::Set), + latitude: self.latitude.map_or(ActiveValue::NotSet, ActiveValue::Set), + longitude: self.longitude.map_or(ActiveValue::NotSet, ActiveValue::Set), + name: self.name.map_or(ActiveValue::NotSet, ActiveValue::Set), + region: self.region.map_or(ActiveValue::NotSet, ActiveValue::Set), + state: self.state.map_or(ActiveValue::NotSet, ActiveValue::Set), + state_abbrev: self + .state_abbrev + .map_or(ActiveValue::NotSet, ActiveValue::Set), + speed_limit: self + .speed_limit + .map_or(ActiveValue::NotSet, ActiveValue::Set), + created_at: ActiveValue::NotSet, + updated_at: ActiveValue::NotSet, + } + } +} diff --git a/entity/src/wrappers/mod.rs b/entity/src/wrappers/mod.rs index 6750ee3..17aca8d 100644 --- a/entity/src/wrappers/mod.rs +++ b/entity/src/wrappers/mod.rs @@ -1,8 +1,10 @@ -use crate::entities::{brokenspoke_pipeline, sea_orm_active_enums, submission}; -use sea_orm::{ - prelude::{Json, TimeDateTimeWithTimeZone, Uuid}, - ActiveValue, IntoActiveModel, -}; +pub mod bna; +pub mod brokenspoke_pipeline; +pub mod census; +pub mod city; +pub mod submission; + +use crate::entities::sea_orm_active_enums; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -78,277 +80,3 @@ impl FromStr for BrokenspokeState { serde_plain::from_str::(s) } } - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct SubmissionPost { - pub first_name: String, - pub last_name: String, - pub title: Option, - pub organization: Option, - pub email: String, - pub country: String, - pub city: String, - pub region: Option, - pub fips_code: String, - pub consent: bool, - pub status: Option, -} - -impl IntoActiveModel for SubmissionPost { - fn into_active_model(self) -> submission::ActiveModel { - submission::ActiveModel { - id: ActiveValue::NotSet, - first_name: ActiveValue::Set(self.first_name), - last_name: ActiveValue::Set(self.last_name), - title: ActiveValue::Set(self.title), - organization: ActiveValue::Set(self.organization), - email: ActiveValue::Set(self.email), - country: ActiveValue::Set(self.country), - city: ActiveValue::Set(self.city), - region: ActiveValue::Set(self.region), - fips_code: ActiveValue::Set(self.fips_code), - consent: ActiveValue::Set(self.consent), - status: self.status.map_or(ActiveValue::NotSet, ActiveValue::Set), - created_at: ActiveValue::NotSet, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct SubmissionPatch { - pub first_name: Option, - pub last_name: Option, - pub title: Option>, - pub organization: Option>, - pub email: Option, - pub country: Option, - pub city: Option, - pub region: Option>, - pub fips_code: Option, - pub consent: Option, - pub status: Option, -} - -impl IntoActiveModel for SubmissionPatch { - fn into_active_model(self) -> submission::ActiveModel { - submission::ActiveModel { - id: ActiveValue::NotSet, - first_name: self - .first_name - .map_or(ActiveValue::NotSet, ActiveValue::Set), - last_name: self.last_name.map_or(ActiveValue::NotSet, ActiveValue::Set), - title: self.title.map_or(ActiveValue::NotSet, ActiveValue::Set), - organization: self - .organization - .map_or(ActiveValue::NotSet, ActiveValue::Set), - email: self.email.map_or(ActiveValue::NotSet, ActiveValue::Set), - country: self.country.map_or(ActiveValue::NotSet, ActiveValue::Set), - city: self.city.map_or(ActiveValue::NotSet, ActiveValue::Set), - region: self.region.map_or(ActiveValue::NotSet, ActiveValue::Set), - fips_code: self.fips_code.map_or(ActiveValue::NotSet, ActiveValue::Set), - consent: self.consent.map_or(ActiveValue::NotSet, ActiveValue::Set), - status: self.status.map_or(ActiveValue::NotSet, ActiveValue::Set), - created_at: ActiveValue::NotSet, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct BrokenspokePipelinePost { - pub state: Option, - pub state_machine_id: Uuid, - pub scheduled_trigger_id: Option, - pub sqs_message: Option, - pub neon_branch_id: Option, - pub fargate_task_arn: Option, - pub s3_bucket: Option, - pub start_time: TimeDateTimeWithTimeZone, - pub end_time: Option, - pub torn_down: Option, -} - -impl IntoActiveModel for BrokenspokePipelinePost { - fn into_active_model(self) -> brokenspoke_pipeline::ActiveModel { - brokenspoke_pipeline::ActiveModel { - state: ActiveValue::Set(self.state), - state_machine_id: ActiveValue::Set(self.state_machine_id), - sqs_message: ActiveValue::Set(self.sqs_message), - neon_branch_id: ActiveValue::Set(self.neon_branch_id), - fargate_task_arn: ActiveValue::Set(self.fargate_task_arn), - s3_bucket: ActiveValue::Set(self.s3_bucket), - scheduled_trigger_id: ActiveValue::Set(self.scheduled_trigger_id), - start_time: ActiveValue::Set(self.start_time), - end_time: ActiveValue::Set(self.end_time), - torn_down: ActiveValue::Set(self.torn_down), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct BrokenspokePipelinePatch { - pub state: Option>, - pub scheduled_trigger_id: Option>, - pub sqs_message: Option>, - pub neon_branch_id: Option>, - pub fargate_task_arn: Option>, - pub s3_bucket: Option>, - pub start_time: Option>, - pub end_time: Option>, - pub torn_down: Option>, -} - -impl IntoActiveModel for BrokenspokePipelinePatch { - fn into_active_model(self) -> brokenspoke_pipeline::ActiveModel { - brokenspoke_pipeline::ActiveModel { - state_machine_id: ActiveValue::NotSet, - state: self.state.map_or(ActiveValue::NotSet, ActiveValue::Set), - sqs_message: self - .sqs_message - .map_or(ActiveValue::NotSet, ActiveValue::Set), - neon_branch_id: self - .neon_branch_id - .map_or(ActiveValue::NotSet, ActiveValue::Set), - fargate_task_arn: self - .fargate_task_arn - .map_or(ActiveValue::NotSet, ActiveValue::Set), - s3_bucket: self.s3_bucket.map_or(ActiveValue::NotSet, ActiveValue::Set), - scheduled_trigger_id: self - .scheduled_trigger_id - .map_or(ActiveValue::NotSet, ActiveValue::Set), - start_time: ActiveValue::NotSet, - end_time: self.end_time.map_or(ActiveValue::NotSet, ActiveValue::Set), - torn_down: self.torn_down.map_or(ActiveValue::NotSet, ActiveValue::Set), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_submission_post_into_active_model_full() { - let first_name = "John".to_string(); - let last_name = "Doe".to_string(); - let title = Some("Director".to_owned()); - let organization = Some("ACME".to_string()); - let email = "john.doe@acme.org".to_string(); - let country = "usa".to_string(); - let city = "austin".to_string(); - let region = Some("texas".to_string()); - let fips_code = "0123456".to_string(); - let consent = true; - let status = None; - let wrapper = SubmissionPost { - first_name: first_name.clone(), - last_name: last_name.clone(), - title: title.clone(), - organization: organization.clone(), - email: email.clone(), - country: country.clone(), - city: city.clone(), - region: region.clone(), - fips_code: fips_code.clone(), - consent, - status: status.clone(), - }; - let active_model = wrapper.into_active_model(); - let expected = submission::ActiveModel { - id: ActiveValue::NotSet, - first_name: ActiveValue::Set(first_name), - last_name: ActiveValue::Set(last_name), - title: ActiveValue::Set(title), - organization: ActiveValue::Set(organization), - email: ActiveValue::Set(email), - country: ActiveValue::Set(country), - city: ActiveValue::Set(city), - region: ActiveValue::Set(region), - fips_code: ActiveValue::Set(fips_code), - consent: ActiveValue::Set(consent), - status: ActiveValue::NotSet, - created_at: ActiveValue::NotSet, - }; - assert_eq!(active_model, expected); - } - - #[test] - fn test_submission_post_into_active_model_required_only() { - let first_name = "John".to_string(); - let last_name = "Doe".to_string(); - let title = None; - let organization = None; - let email = "john.doe@acme.org".to_string(); - let country = "usa".to_string(); - let city = "austin".to_string(); - let region = None; - let fips_code = "0123456".to_string(); - let consent = true; - let status = Some(sea_orm_active_enums::ApprovalStatus::Approved); - let wrapper = SubmissionPost { - first_name: first_name.clone(), - last_name: last_name.clone(), - title: title.clone(), - organization: organization.clone(), - email: email.clone(), - country: country.clone(), - city: city.clone(), - region: region.clone(), - fips_code: fips_code.clone(), - consent, - status: status.clone(), - }; - let active_model = wrapper.into_active_model(); - let expected = submission::ActiveModel { - id: ActiveValue::NotSet, - first_name: ActiveValue::Set(first_name), - last_name: ActiveValue::Set(last_name), - title: ActiveValue::Set(title), - organization: ActiveValue::Set(organization), - email: ActiveValue::Set(email), - country: ActiveValue::Set(country), - city: ActiveValue::Set(city), - region: ActiveValue::Set(region), - fips_code: ActiveValue::Set(fips_code), - consent: ActiveValue::Set(consent), - status: ActiveValue::Set(sea_orm_active_enums::ApprovalStatus::Approved), - created_at: ActiveValue::NotSet, - }; - assert_eq!(active_model, expected); - } - - #[test] - fn test_submission_patch_into_model() { - let id = 42; - let first_name = "John".to_string(); - let wrapper = SubmissionPatch { - first_name: Some(first_name.clone()), - last_name: None, - title: None, - organization: None, - email: None, - country: None, - city: None, - region: None, - fips_code: None, - consent: None, - status: None, - }; - let active_model = wrapper.into_active_model(); - let expected = submission::ActiveModel { - id: ActiveValue::Unchanged(id), - first_name: ActiveValue::Set(first_name), - last_name: ActiveValue::NotSet, - title: ActiveValue::NotSet, - organization: ActiveValue::NotSet, - email: ActiveValue::NotSet, - country: ActiveValue::NotSet, - city: ActiveValue::NotSet, - region: ActiveValue::NotSet, - fips_code: ActiveValue::NotSet, - consent: ActiveValue::NotSet, - status: ActiveValue::NotSet, - created_at: ActiveValue::NotSet, - }; - assert_eq!(active_model, expected); - } -} diff --git a/entity/src/wrappers/submission.rs b/entity/src/wrappers/submission.rs new file mode 100644 index 0000000..8566984 --- /dev/null +++ b/entity/src/wrappers/submission.rs @@ -0,0 +1,208 @@ +use crate::entities::{sea_orm_active_enums, submission}; +use sea_orm::{ActiveValue, IntoActiveModel}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SubmissionPost { + pub first_name: String, + pub last_name: String, + pub title: Option, + pub organization: Option, + pub email: String, + pub country: String, + pub city: String, + pub region: Option, + pub fips_code: String, + pub consent: bool, + pub status: Option, +} + +impl IntoActiveModel for SubmissionPost { + fn into_active_model(self) -> submission::ActiveModel { + submission::ActiveModel { + id: ActiveValue::NotSet, + first_name: ActiveValue::Set(self.first_name), + last_name: ActiveValue::Set(self.last_name), + title: ActiveValue::Set(self.title), + organization: ActiveValue::Set(self.organization), + email: ActiveValue::Set(self.email), + country: ActiveValue::Set(self.country), + city: ActiveValue::Set(self.city), + region: ActiveValue::Set(self.region), + fips_code: ActiveValue::Set(self.fips_code), + consent: ActiveValue::Set(self.consent), + status: self.status.map_or(ActiveValue::NotSet, ActiveValue::Set), + created_at: ActiveValue::NotSet, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SubmissionPatch { + pub first_name: Option, + pub last_name: Option, + pub title: Option>, + pub organization: Option>, + pub email: Option, + pub country: Option, + pub city: Option, + pub region: Option>, + pub fips_code: Option, + pub consent: Option, + pub status: Option, +} + +impl IntoActiveModel for SubmissionPatch { + fn into_active_model(self) -> submission::ActiveModel { + submission::ActiveModel { + id: ActiveValue::NotSet, + first_name: self + .first_name + .map_or(ActiveValue::NotSet, ActiveValue::Set), + last_name: self.last_name.map_or(ActiveValue::NotSet, ActiveValue::Set), + title: self.title.map_or(ActiveValue::NotSet, ActiveValue::Set), + organization: self + .organization + .map_or(ActiveValue::NotSet, ActiveValue::Set), + email: self.email.map_or(ActiveValue::NotSet, ActiveValue::Set), + country: self.country.map_or(ActiveValue::NotSet, ActiveValue::Set), + city: self.city.map_or(ActiveValue::NotSet, ActiveValue::Set), + region: self.region.map_or(ActiveValue::NotSet, ActiveValue::Set), + fips_code: self.fips_code.map_or(ActiveValue::NotSet, ActiveValue::Set), + consent: self.consent.map_or(ActiveValue::NotSet, ActiveValue::Set), + status: self.status.map_or(ActiveValue::NotSet, ActiveValue::Set), + created_at: ActiveValue::NotSet, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_submission_post_into_active_model_full() { + let first_name = "John".to_string(); + let last_name = "Doe".to_string(); + let title = Some("Director".to_owned()); + let organization = Some("ACME".to_string()); + let email = "john.doe@acme.org".to_string(); + let country = "usa".to_string(); + let city = "austin".to_string(); + let region = Some("texas".to_string()); + let fips_code = "0123456".to_string(); + let consent = true; + let status = None; + let wrapper = SubmissionPost { + first_name: first_name.clone(), + last_name: last_name.clone(), + title: title.clone(), + organization: organization.clone(), + email: email.clone(), + country: country.clone(), + city: city.clone(), + region: region.clone(), + fips_code: fips_code.clone(), + consent, + status: status.clone(), + }; + let active_model = wrapper.into_active_model(); + let expected = submission::ActiveModel { + id: ActiveValue::NotSet, + first_name: ActiveValue::Set(first_name), + last_name: ActiveValue::Set(last_name), + title: ActiveValue::Set(title), + organization: ActiveValue::Set(organization), + email: ActiveValue::Set(email), + country: ActiveValue::Set(country), + city: ActiveValue::Set(city), + region: ActiveValue::Set(region), + fips_code: ActiveValue::Set(fips_code), + consent: ActiveValue::Set(consent), + status: ActiveValue::NotSet, + created_at: ActiveValue::NotSet, + }; + assert_eq!(active_model, expected); + } + + #[test] + fn test_submission_post_into_active_model_required_only() { + let first_name = "John".to_string(); + let last_name = "Doe".to_string(); + let title = None; + let organization = None; + let email = "john.doe@acme.org".to_string(); + let country = "usa".to_string(); + let city = "austin".to_string(); + let region = None; + let fips_code = "0123456".to_string(); + let consent = true; + let status = Some(sea_orm_active_enums::ApprovalStatus::Approved); + let wrapper = SubmissionPost { + first_name: first_name.clone(), + last_name: last_name.clone(), + title: title.clone(), + organization: organization.clone(), + email: email.clone(), + country: country.clone(), + city: city.clone(), + region: region.clone(), + fips_code: fips_code.clone(), + consent, + status: status.clone(), + }; + let active_model = wrapper.into_active_model(); + let expected = submission::ActiveModel { + id: ActiveValue::NotSet, + first_name: ActiveValue::Set(first_name), + last_name: ActiveValue::Set(last_name), + title: ActiveValue::Set(title), + organization: ActiveValue::Set(organization), + email: ActiveValue::Set(email), + country: ActiveValue::Set(country), + city: ActiveValue::Set(city), + region: ActiveValue::Set(region), + fips_code: ActiveValue::Set(fips_code), + consent: ActiveValue::Set(consent), + status: ActiveValue::Set(sea_orm_active_enums::ApprovalStatus::Approved), + created_at: ActiveValue::NotSet, + }; + assert_eq!(active_model, expected); + } + + #[test] + fn test_submission_patch_into_model() { + let id = 42; + let first_name = "John".to_string(); + let wrapper = SubmissionPatch { + first_name: Some(first_name.clone()), + last_name: None, + title: None, + organization: None, + email: None, + country: None, + city: None, + region: None, + fips_code: None, + consent: None, + status: None, + }; + let active_model = wrapper.into_active_model(); + let expected = submission::ActiveModel { + id: ActiveValue::Unchanged(id), + first_name: ActiveValue::Set(first_name), + last_name: ActiveValue::NotSet, + title: ActiveValue::NotSet, + organization: ActiveValue::NotSet, + email: ActiveValue::NotSet, + country: ActiveValue::NotSet, + city: ActiveValue::NotSet, + region: ActiveValue::NotSet, + fips_code: ActiveValue::NotSet, + consent: ActiveValue::NotSet, + status: ActiveValue::NotSet, + created_at: ActiveValue::NotSet, + }; + assert_eq!(active_model, expected); + } +} diff --git a/examples/query.rs b/examples/query.rs index 1f225b9..f8b6284 100644 --- a/examples/query.rs +++ b/examples/query.rs @@ -62,6 +62,17 @@ async fn main() -> Result<(), Report> { // .await?; // dbg!(summ); + // SELECT SUMMARY.SCORE, + // CORE_SERVICES.SCORE AS CSSCORE, + // INFRASTRUCTURE.low_Stress_miles, + // FROM SUMMARY + // JOIN CORE_SERVICES ON CORE_SERVICES.BNA_UUID = SUMMARY.BNA_UUID + // JOIN INFRASTRUCTURE ON INFRASTRUCTURE.BNA_UUID = SUMMARY.BNA_UUID + // JOIN RECREATION ON RECREATION.BNA_UUID = SUMMARY.BNA_UUID + // JOIN OPPORTUNITY ON OPPORTUNITY.BNA_UUID = SUMMARY.BNA_UUID + // JOIN FEATURES ON FEATURES.BNA_UUID = SUMMARY.BNA_UUID + // WHERE SUMMARY.BNA_UUID = '18ca9450-2bfa-43a0-b8f3-2cd16952eba1' ; + let submission_model = entity::submission::ActiveModel { id: ActiveValue::NotSet, first_name: ActiveValue::Set("Floyd".to_string()), @@ -84,14 +95,3 @@ async fn main() -> Result<(), Report> { Ok(()) } - -// SELECT SUMMARY.SCORE, -// CORE_SERVICES.SCORE AS CSSCORE, -// INFRASTRUCTURE.low_Stress_miles, -// FROM SUMMARY -// JOIN CORE_SERVICES ON CORE_SERVICES.BNA_UUID = SUMMARY.BNA_UUID -// JOIN INFRASTRUCTURE ON INFRASTRUCTURE.BNA_UUID = SUMMARY.BNA_UUID -// JOIN RECREATION ON RECREATION.BNA_UUID = SUMMARY.BNA_UUID -// JOIN OPPORTUNITY ON OPPORTUNITY.BNA_UUID = SUMMARY.BNA_UUID -// JOIN FEATURES ON FEATURES.BNA_UUID = SUMMARY.BNA_UUID -// WHERE SUMMARY.BNA_UUID = '18ca9450-2bfa-43a0-b8f3-2cd16952eba1' ; diff --git a/examples/seeder.rs b/examples/seeder.rs index 63443a5..7882a97 100644 --- a/examples/seeder.rs +++ b/examples/seeder.rs @@ -82,6 +82,9 @@ async fn main() -> Result<(), Report> { let year = scorecard.year - 2000; let version = Calver::try_from_ubuntu(format!("{year}.1").as_str()).unwrap(); + // Get the records creation date. + let created_at = scorecard.creation_date; + // Get the City UUID. let city_uuid = Uuid::parse_str(&scorecard.bna_id).unwrap(); @@ -113,7 +116,7 @@ async fn main() -> Result<(), Report> { state: ActiveValue::Set(scorecard.state_full), state_abbrev: ActiveValue::Set(scorecard.state), speed_limit: ActiveValue::Set(city_speed_limit), - created_at: ActiveValue::NotSet, + created_at: ActiveValue::Set(created_at), updated_at: ActiveValue::NotSet, }; cities.insert(city_uuid, city_model); @@ -123,7 +126,7 @@ async fn main() -> Result<(), Report> { let census_model = census::ActiveModel { census_id: ActiveValue::NotSet, city_id: ActiveValue::Set(city_uuid), - created_at: ActiveValue::NotSet, + created_at: ActiveValue::Set(created_at), fips_code: scorecard .census_fips_code .map_or(ActiveValue::NotSet, |v| ActiveValue::Set(v.to_string())), @@ -140,7 +143,7 @@ async fn main() -> Result<(), Report> { let speed_limit_model = speed_limit::ActiveModel { speed_limit_id: ActiveValue::NotSet, city_id: ActiveValue::Set(city_uuid), - created_at: ActiveValue::NotSet, + created_at: ActiveValue::Set(created_at), residential: scorecard .residential_speed_limit .map_or(ActiveValue::NotSet, |v| ActiveValue::Set(v.into())), @@ -152,7 +155,7 @@ async fn main() -> Result<(), Report> { let summary_model = summary::ActiveModel { bna_uuid: ActiveValue::Set(bna_uuid), city_id: ActiveValue::Set(city_uuid), - created_at: ActiveValue::NotSet, + created_at: ActiveValue::Set(created_at), score: ActiveValue::Set(scorecard.bna_rounded_score.into()), version: ActiveValue::Set(version.to_ubuntu()), }; diff --git a/lambdas/Cargo.toml b/lambdas/Cargo.toml index 0ade1ce..20a3f6c 100644 --- a/lambdas/Cargo.toml +++ b/lambdas/Cargo.toml @@ -14,14 +14,10 @@ effortless = { path = "../effortless" } entity = { path = "../entity" } http-serde = { workspace = true } lambda_http = { workspace = true } -lambda_runtime = "0.10" +lambda_runtime = "0.11.0" nom = "7.1.3" once_cell = { workspace = true } -reqwest = { version = "0.11.22", features = [ - "json", - "native-tls-vendored", - "rustls", -] } +reqwest = { version = "0.12.1", features = ["json", "native-tls-vendored"] } sea-orm = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -33,19 +29,26 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [ ] } url = "2.3.1" - [[bin]] name = "get-bnas" path = "src/bnas/get-bnas.rs" [[bin]] -name = "get-bnas-analysis" -path = "src/bnas-analysis/get-bnas-analysis.rs" +name = "post-bnas" +path = "src/bnas/post-bnas.rs" + +# [[bin]] +# name = "patch-bnas" +# path = "src/bnas/patch-bnas.rs" [[bin]] name = "get-bnas-cities" path = "src/bnas/get-bnas-cities.rs" +[[bin]] +name = "get-bnas-analysis" +path = "src/bnas-analysis/get-bnas-analysis.rs" + [[bin]] name = "get-cities" path = "src/cities/get-cities.rs" @@ -54,6 +57,10 @@ path = "src/cities/get-cities.rs" name = "get-cities-bnas" path = "src/cities/get-cities-bnas.rs" +[[bin]] +name = "get-cities-census" +path = "src/cities/get-cities-census.rs" + [[bin]] name = "get-cities-submissions" path = "src/cities-submissions/get-cities-submissions.rs" diff --git a/lambdas/requests.rest b/lambdas/requests.rest index 3fc0630..21e07e8 100644 --- a/lambdas/requests.rest +++ b/lambdas/requests.rest @@ -27,6 +27,10 @@ GET {{host}}/cities/{{city_id}} # Query all the BNAs of a specific city. GET {{host}}/cities/{{city_id}}/bnas +### +# Query all the Census of a specific city. +GET {{host}}/cities/{{city_id}}/census + ### # Query all the submissions. GET {{host}}/cities/submissions diff --git a/lambdas/src/bnas-analysis/get-bnas-analysis.rs b/lambdas/src/bnas-analysis/get-bnas-analysis.rs index 0064296..ef50940 100644 --- a/lambdas/src/bnas-analysis/get-bnas-analysis.rs +++ b/lambdas/src/bnas-analysis/get-bnas-analysis.rs @@ -49,13 +49,15 @@ async fn function_handler(event: Request) -> Result, Error> { } }, None => { - let query = BrokenspokePipeline::find() + let select = BrokenspokePipeline::find(); + let query = select + .clone() .paginate(&db, page_size) .fetch_page(page - 1) .await; let res: Response = match query { Ok(models) => { - let total_items = BrokenspokePipeline::find().count(&db).await?; + let total_items = select.count(&db).await?; build_paginated_response(json!(models), total_items, page, page_size, &event)? } Err(e) => APIError::with_pointer( diff --git a/lambdas/src/bnas-analysis/patch-bnas-analysis.rs b/lambdas/src/bnas-analysis/patch-bnas-analysis.rs index 7fd66e7..c7f32da 100644 --- a/lambdas/src/bnas-analysis/patch-bnas-analysis.rs +++ b/lambdas/src/bnas-analysis/patch-bnas-analysis.rs @@ -4,7 +4,10 @@ use effortless::{ error::APIErrors, fragment::BnaRequestExt, }; -use entity::{brokenspoke_pipeline::ActiveModel, prelude::*, wrappers}; +use entity::{ + brokenspoke_pipeline::ActiveModel, prelude::*, + wrappers::brokenspoke_pipeline::BrokenspokePipelinePatch, +}; use lambda_http::{run, service_fn, Body, Error, IntoResponse, Request, Response}; use lambdas::database_connect; use sea_orm::{prelude::Uuid, ActiveValue, EntityTrait, IntoActiveModel}; @@ -42,7 +45,7 @@ pub fn prepare_active_model(event: &Request) -> Result { .map_err(|e| invalid_path_parameter(event, parameter, e.to_string().as_str()))?; // Extract and deserialize the data. - let wrapper = parse_request_body::(event)?; + let wrapper = parse_request_body::(event)?; // Turn the wrapper into an active model. let mut active_model = wrapper.into_active_model(); diff --git a/lambdas/src/bnas-analysis/post-bnas-analysis.rs b/lambdas/src/bnas-analysis/post-bnas-analysis.rs index 200d0af..c5d26a5 100644 --- a/lambdas/src/bnas-analysis/post-bnas-analysis.rs +++ b/lambdas/src/bnas-analysis/post-bnas-analysis.rs @@ -1,6 +1,6 @@ use dotenv::dotenv; use effortless::api::parse_request_body; -use entity::{prelude::*, wrappers}; +use entity::{prelude::*, wrappers::brokenspoke_pipeline::BrokenspokePipelinePost}; use lambda_http::{run, service_fn, Body, Error, IntoResponse, Request, Response}; use lambdas::database_connect; use sea_orm::{EntityTrait, IntoActiveModel}; @@ -11,7 +11,7 @@ async fn function_handler(event: Request) -> Result, Error> { dotenv().ok(); // Extract and serialize the data. - let wrapper = match parse_request_body::(&event) { + let wrapper = match parse_request_body::(&event) { Ok(value) => value, Err(e) => return Ok(e.into()), }; diff --git a/lambdas/src/bnas/get-bnas.rs b/lambdas/src/bnas/get-bnas.rs index d461d77..471f535 100644 --- a/lambdas/src/bnas/get-bnas.rs +++ b/lambdas/src/bnas/get-bnas.rs @@ -1,12 +1,76 @@ use dotenv::dotenv; -use effortless::api::{entry_not_found, parse_path_parameter}; -use entity::summary; +use effortless::api::{entry_not_found, parse_path_parameter, parse_query_string_parameter}; +use entity::{core_services, features, infrastructure, opportunity, prelude::*, recreation}; use lambda_http::{run, service_fn, Body, Error, IntoResponse, Request, Response}; use lambdas::{api_database_connect, build_paginated_response, pagination_parameters}; -use sea_orm::{prelude::Uuid, EntityTrait, PaginatorTrait}; +use sea_orm::{ + prelude::Uuid, EntityTrait, FromQueryResult, JoinType, PaginatorTrait, QuerySelect, + RelationTrait, +}; +use serde::{Deserialize, Serialize}; use serde_json::json; +use std::str::FromStr; use tracing::{debug, info}; +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum BNAComponent { + All, + Summary, + Infratructure, + Recreation, + Opportunity, + CoreServices, + Features, +} + +impl FromStr for BNAComponent { + type Err = serde_plain::Error; + + fn from_str(s: &str) -> Result { + serde_plain::from_str::(s) + } +} + +#[derive(FromQueryResult, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BNA { + // BNA Summary + pub bna_uuid: Uuid, + pub city_id: Uuid, + pub score: f64, + pub version: String, + + // BNAInfrastructure + pub low_stress_miles: Option, + pub high_stress_miles: Option, + + // BNA Recreation + pub community_centers: Option, + pub parks: Option, + pub recreation_trails: Option, + pub recreation_score: Option, + + // BNA Opportunity + pub employment: Option, + pub higher_education: Option, + pub k12_education: Option, + pub opportunity_score: Option, + pub technical_vocational_college: Option, + + // BNA Core Services + pub dentists: Option, + pub doctors: Option, + pub grocery: Option, + pub hospitals: Option, + pub pharmacies: Option, + pub coreservices_score: Option, + pub social_services: Option, + + // BNA Features + pub people: Option, + pub retail: Option, + pub transit: Option, +} + async fn function_handler(event: Request) -> Result, Error> { dotenv().ok(); @@ -22,14 +86,146 @@ async fn function_handler(event: Request) -> Result, Error> { Err(e) => return Ok(e), }; + // Retrieve the component param if any. + let component = match parse_query_string_parameter::(&event, "component") { + Ok(component) => match component { + Some(component) => component, + None => BNAComponent::All, + }, + Err(e) => return Ok(e.into()), + }; + // Retrieve all bnas or a specific one. debug!("Processing the requests..."); + match id { Some(id) => { - let model = summary::Entity::find_by_id(id).one(&db).await?; - let res: Response = match model { - Some(model) => json!(model).into_response().await, - None => entry_not_found(&event).into(), + let select = Summary::find_by_id(id); + let res = match component { + BNAComponent::All => { + let model = select + .clone() + .columns([ + entity::core_services::Column::Dentists, + entity::core_services::Column::Doctors, + entity::core_services::Column::Grocery, + entity::core_services::Column::Hospitals, + entity::core_services::Column::Pharmacies, + entity::core_services::Column::SocialServices, + ]) + .column_as(entity::core_services::Column::Score, "coreservices_score") + .columns([ + entity::infrastructure::Column::HighStressMiles, + entity::infrastructure::Column::LowStressMiles, + ]) + .columns([ + entity::recreation::Column::CommunityCenters, + entity::recreation::Column::Parks, + entity::recreation::Column::RecreationTrails, + ]) + .column_as(entity::recreation::Column::Score, "recreation_score") + .columns([ + entity::opportunity::Column::Employment, + entity::opportunity::Column::HigherEducation, + entity::opportunity::Column::K12Education, + entity::opportunity::Column::TechnicalVocationalCollege, + ]) + .column_as(entity::opportunity::Column::Score, "opportunity_score") + .columns([ + entity::features::Column::People, + entity::features::Column::Retail, + entity::features::Column::Transit, + ]) + .join( + JoinType::InnerJoin, + entity::summary::Relation::CoreServices.def(), + ) + .join( + JoinType::InnerJoin, + entity::summary::Relation::Infrastructure.def(), + ) + .join( + JoinType::InnerJoin, + entity::summary::Relation::Recreation.def(), + ) + .join( + JoinType::InnerJoin, + entity::summary::Relation::Opportunity.def(), + ) + .join( + sea_orm::JoinType::InnerJoin, + entity::summary::Relation::Features.def(), + ) + .into_model::() + .one(&db) + .await?; + match model { + Some(model) => json!(model).into_response().await, + None => entry_not_found(&event).into(), + } + } + BNAComponent::Summary => { + let model = select.clone().one(&db).await?; + match model { + Some(model) => json!(model).into_response().await, + None => entry_not_found(&event).into(), + } + } + BNAComponent::Infratructure => { + let model = select + .clone() + .find_also_related(infrastructure::Entity) + .one(&db) + .await?; + match model { + Some(model) => json!(model).into_response().await, + None => entry_not_found(&event).into(), + } + } + BNAComponent::Recreation => { + let model = select + .clone() + .find_also_related(recreation::Entity) + .one(&db) + .await?; + match model { + Some(model) => json!(model).into_response().await, + None => entry_not_found(&event).into(), + } + } + BNAComponent::Opportunity => { + let model = select + .clone() + .find_also_related(opportunity::Entity) + .one(&db) + .await?; + match model { + Some(model) => json!(model).into_response().await, + None => entry_not_found(&event).into(), + } + } + BNAComponent::CoreServices => { + let model = select + .clone() + .find_also_related(core_services::Entity) + .one(&db) + .await?; + match model { + Some(model) => json!(model).into_response().await, + None => entry_not_found(&event).into(), + } + } + BNAComponent::Features => { + let model = select + .clone() + .find_also_related(features::Entity) + .one(&db) + .await?; + match model { + Some(model) => json!(model).into_response().await, + None => entry_not_found(&event).into(), + } + } }; Ok(res) } @@ -41,11 +237,13 @@ async fn function_handler(event: Request) -> Result, Error> { }; // Retrieve entries. - let body = summary::Entity::find() + let select = Summary::find(); + let body = select + .clone() .paginate(&db, page_size) .fetch_page(page - 1) .await?; - let total_items = summary::Entity::find().count(&db).await?; + let total_items = select.count(&db).await?; build_paginated_response(json!(body), total_items, page, page_size, &event) } } @@ -66,3 +264,50 @@ async fn main() -> Result<(), Error> { e }) } + +// #[cfg(test)] +// mod tests { + +// use super::*; +// use aws_lambda_events::http; +// use lambda_http::RequestExt; +// use std::collections::HashMap; + +// #[tokio::test] +// async fn test_handler_all() { +// let event = http::Request::builder() +// .header(http::header::CONTENT_TYPE, "application/json") +// .body(Body::Empty) +// .expect("failed to build request") +// .with_path_parameters(HashMap::from([( +// "id".to_string(), +// "837082b8-c8a0-469e-b310-c868d7f140a2".to_string(), // Santa Monica, CA +// )])) +// .with_request_context(lambda_http::request::RequestContext::ApiGatewayV2( +// lambda_http::aws_lambda_events::apigw::ApiGatewayV2httpRequestContext::default(), +// )); +// let r = function_handler(event).await.unwrap(); +// dbg!(r); +// } + +// #[tokio::test] +// async fn test_handler_opportunity() { +// let event = http::Request::builder() +// .header(http::header::CONTENT_TYPE, "application/json") +// .body(Body::Empty) +// .expect("failed to build request") +// .with_path_parameters(HashMap::from([( +// "id".to_string(), +// "837082b8-c8a0-469e-b310-c868d7f140a2".to_string(), // Santa Monica, CA +// )])) +// .with_query_string_parameters(HashMap::from([( +// "component".to_string(), +// "Opportunity".to_string(), +// )])) +// .with_request_context(lambda_http::request::RequestContext::ApiGatewayV2( +// lambda_http::aws_lambda_events::apigw::ApiGatewayV2httpRequestContext::default(), +// )); +// let r = function_handler(event).await.unwrap(); +// dbg!(r); +// } +// } diff --git a/lambdas/src/bnas/patch-bnas.rs b/lambdas/src/bnas/patch-bnas.rs new file mode 100644 index 0000000..784883a --- /dev/null +++ b/lambdas/src/bnas/patch-bnas.rs @@ -0,0 +1,102 @@ +use dotenv::dotenv; +use effortless::{ + api::{ + entry_not_found, invalid_path_parameter, missing_parameter, parse_path_parameter, + parse_request_body, + }, + fragment::BnaRequestExt, +}; +use entity::{ + core_services, features, infrastructure, opportunity, prelude, recreation, summary, + wrappers::bna::BNAPatch, +}; +use lambda_http::{run, service_fn, Body, Error, IntoResponse, Request, Response}; +use lambdas::database_connect; +use sea_orm::{prelude::Uuid, ActiveValue, EntityTrait}; +use serde_json::json; +use tracing::info; + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + // disable printing the name of the module in every log line. + .with_target(false) + // disabling time is handy because CloudWatch will add the ingestion time. + .without_time() + .init(); + + run(service_fn(function_handler)).await.map_err(|e| { + info!("{e}"); + e + }) +} + +async fn function_handler(event: Request) -> Result, Error> { + dotenv().ok(); + + // Retrieve the ID of the entry to update. + let parameter = "id"; + let id = event + .path_parameter::(parameter) + .ok_or(missing_parameter(&event, parameter))? + .map_err(|e| invalid_path_parameter(&event, parameter, e.to_string().as_str()))?; + + // Extract and deserialize the data. + let wrapper = match parse_request_body::(&event) { + Ok(value) => value, + Err(e) => return Ok(e.into()), + }; + + // Turn the model wrapper into active models. + let summary = summary::ActiveModel { + bna_uuid: ActiveValue::NotSet, + city_id: wrapper + .summary + .city_id + .map_or(ActiveValue::NotSet, ActiveValue::Set), + created_at: ActiveValue::NotSet, + score: ActiveValue::Set(wrapper.summary.score), + version: ActiveValue::Set(wrapper.summary.version), + }; + let core_services = core_services::ActiveModel { + bna_uuid: ActiveValue::NotSet, + dentists: ActiveValue::Set(wrapper.core_services.dentists), + doctors: ActiveValue::Set(wrapper.core_services.doctors), + grocery: ActiveValue::Set(wrapper.core_services.grocery), + hospitals: ActiveValue::Set(wrapper.core_services.hospitals), + pharmacies: ActiveValue::Set(wrapper.core_services.pharmacies), + score: ActiveValue::Set(wrapper.core_services.score), + social_services: ActiveValue::Set(wrapper.core_services.social_services), + }; + let features = features::ActiveModel { + bna_uuid: ActiveValue::NotSet, + people: ActiveValue::Set(wrapper.features.people), + retail: ActiveValue::Set(wrapper.features.retail), + transit: ActiveValue::Set(wrapper.features.transit), + }; + let infrastructure = infrastructure::ActiveModel { + bna_uuid: ActiveValue::NotSet, + low_stress_miles: ActiveValue::Set(wrapper.infrastructure.low_stress_miles), + high_stress_miles: ActiveValue::Set(wrapper.infrastructure.high_stress_miles), + }; + let opportunity = opportunity::ActiveModel { + bna_uuid: ActiveValue::NotSet, + employment: ActiveValue::Set(wrapper.opportunity.employment), + higher_education: ActiveValue::Set(wrapper.opportunity.higher_education), + k12_education: ActiveValue::Set(wrapper.opportunity.k12_education), + score: ActiveValue::Set(wrapper.opportunity.score), + technical_vocational_college: ActiveValue::Set( + wrapper.opportunity.technical_vocational_college, + ), + }; + let recreation = recreation::ActiveModel { + bna_uuid: ActiveValue::NotSet, + community_centers: ActiveValue::Set(wrapper.recreation.community_centers), + parks: ActiveValue::Set(wrapper.recreation.parks), + recreation_trails: ActiveValue::Set(wrapper.recreation.recreation_trails), + score: ActiveValue::Set(wrapper.recreation.score), + }; + + Ok(Body::Empty.into_response().await) +} diff --git a/lambdas/src/bnas/post-bnas.rs b/lambdas/src/bnas/post-bnas.rs new file mode 100644 index 0000000..50ae083 --- /dev/null +++ b/lambdas/src/bnas/post-bnas.rs @@ -0,0 +1,107 @@ +use dotenv::dotenv; +use effortless::api::parse_request_body; +use entity::{ + core_services, features, infrastructure, opportunity, prelude::*, recreation, summary, + wrappers::bna::BNAPost, +}; +use lambda_http::{run, service_fn, Body, Error, IntoResponse, Request, Response}; +use lambdas::database_connect; +use sea_orm::{ActiveValue, EntityTrait}; +use serde_json::json; +use tracing::info; + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + // disable printing the name of the module in every log line. + .with_target(false) + // disabling time is handy because CloudWatch will add the ingestion time. + .without_time() + .init(); + + run(service_fn(function_handler)).await.map_err(|e| { + info!("{e}"); + e + }) +} + +async fn function_handler(event: Request) -> Result, Error> { + dotenv().ok(); + + // Extract and serialize the data. + let wrapper = match parse_request_body::(&event) { + Ok(value) => value, + Err(e) => return Ok(e.into()), + }; + + // Turn the model wrapper into active models. + let summary = summary::ActiveModel { + bna_uuid: ActiveValue::Set(wrapper.summary.bna_uuid), + city_id: ActiveValue::Set(wrapper.summary.city_id), + created_at: ActiveValue::NotSet, + score: ActiveValue::Set(wrapper.summary.score), + version: ActiveValue::Set(wrapper.summary.version), + }; + let core_services = core_services::ActiveModel { + bna_uuid: ActiveValue::Set(wrapper.summary.bna_uuid), + dentists: ActiveValue::Set(wrapper.core_services.dentists), + doctors: ActiveValue::Set(wrapper.core_services.doctors), + grocery: ActiveValue::Set(wrapper.core_services.grocery), + hospitals: ActiveValue::Set(wrapper.core_services.hospitals), + pharmacies: ActiveValue::Set(wrapper.core_services.pharmacies), + score: ActiveValue::Set(wrapper.core_services.score), + social_services: ActiveValue::Set(wrapper.core_services.social_services), + }; + let features = features::ActiveModel { + bna_uuid: ActiveValue::Set(wrapper.summary.bna_uuid), + people: ActiveValue::Set(wrapper.features.people), + retail: ActiveValue::Set(wrapper.features.retail), + transit: ActiveValue::Set(wrapper.features.transit), + }; + let infrastructure = infrastructure::ActiveModel { + bna_uuid: ActiveValue::Set(wrapper.summary.bna_uuid), + low_stress_miles: ActiveValue::Set(wrapper.infrastructure.low_stress_miles), + high_stress_miles: ActiveValue::Set(wrapper.infrastructure.high_stress_miles), + }; + let opportunity = opportunity::ActiveModel { + bna_uuid: ActiveValue::Set(wrapper.summary.bna_uuid), + employment: ActiveValue::Set(wrapper.opportunity.employment), + higher_education: ActiveValue::Set(wrapper.opportunity.higher_education), + k12_education: ActiveValue::Set(wrapper.opportunity.k12_education), + score: ActiveValue::Set(wrapper.opportunity.score), + technical_vocational_college: ActiveValue::Set( + wrapper.opportunity.technical_vocational_college, + ), + }; + let recreation = recreation::ActiveModel { + bna_uuid: ActiveValue::Set(wrapper.summary.bna_uuid), + community_centers: ActiveValue::Set(wrapper.recreation.community_centers), + parks: ActiveValue::Set(wrapper.recreation.parks), + recreation_trails: ActiveValue::Set(wrapper.recreation.recreation_trails), + score: ActiveValue::Set(wrapper.recreation.score), + }; + + // Get the database connection. + let db = database_connect(Some("DATABASE_URL_SECRET_ID")).await?; + + // And insert a new entry for each model. + let summary_res = Summary::insert(summary).exec(&db).await?; + let core_services_res = CoreServices::insert(core_services).exec(&db).await?; + let features_res = Features::insert(features).exec(&db).await?; + let infrastructure_res = Infrastructure::insert(infrastructure).exec(&db).await?; + let opportunity_res = Opportunity::insert(opportunity).exec(&db).await?; + let recreation_res = Recreation::insert(recreation).exec(&db).await?; + Ok(json!(vec![ + summary_res.last_insert_id, + core_services_res.last_insert_id, + features_res.last_insert_id, + infrastructure_res.last_insert_id, + opportunity_res.last_insert_id, + recreation_res.last_insert_id, + ]) + .into_response() + .await) + + // Ok(Body::Empty.into_response().await) +} diff --git a/lambdas/src/cities-submissions/get-cities-submissions.rs b/lambdas/src/cities-submissions/get-cities-submissions.rs index 38f138c..7956d09 100644 --- a/lambdas/src/cities-submissions/get-cities-submissions.rs +++ b/lambdas/src/cities-submissions/get-cities-submissions.rs @@ -62,14 +62,15 @@ async fn function_handler(event: Request) -> Result, Error> { res } None => { - let query = Submission::find() - .filter(conditions.clone()) + let select = Submission::find().filter(conditions); + let query = select + .clone() .paginate(&db, page_size) .fetch_page(page - 1) .await; let res: Response = match query { Ok(models) => { - let total_items = Submission::find().filter(conditions).count(&db).await?; + let total_items = select.count(&db).await?; build_paginated_response(json!(models), total_items, page, page_size, &event)? } Err(e) => { diff --git a/lambdas/src/cities-submissions/patch-cities-submissions.rs b/lambdas/src/cities-submissions/patch-cities-submissions.rs index c98afef..73eddd8 100644 --- a/lambdas/src/cities-submissions/patch-cities-submissions.rs +++ b/lambdas/src/cities-submissions/patch-cities-submissions.rs @@ -3,7 +3,7 @@ use effortless::{ api::{missing_parameter, parse_path_parameter, parse_request_body}, error::APIErrors, }; -use entity::{prelude::*, submission::ActiveModel, wrappers}; +use entity::{prelude::*, submission::ActiveModel, wrappers::submission::SubmissionPatch}; use lambda_http::{run, service_fn, Body, Error, IntoResponse, Request, Response}; use lambdas::database_connect; use sea_orm::{ActiveValue, EntityTrait, IntoActiveModel}; @@ -55,7 +55,7 @@ pub fn prepare_active_model(event: &Request) -> Result { .ok_or(missing_parameter(event, parameter))?; // Extract and deserialize the data. - let wrapper = parse_request_body::(event)?; + let wrapper = parse_request_body::(event)?; // Turn the wrapper into an active model. let mut active_submission = wrapper.into_active_model(); diff --git a/lambdas/src/cities-submissions/post-cities-submissions.rs b/lambdas/src/cities-submissions/post-cities-submissions.rs index 060263b..119f9b3 100644 --- a/lambdas/src/cities-submissions/post-cities-submissions.rs +++ b/lambdas/src/cities-submissions/post-cities-submissions.rs @@ -1,6 +1,6 @@ use dotenv::dotenv; use effortless::api::parse_request_body; -use entity::{prelude::*, wrappers}; +use entity::{prelude::*, wrappers::submission::SubmissionPost}; use lambda_http::{run, service_fn, Body, Error, IntoResponse, Request, Response}; use lambdas::database_connect; use sea_orm::{EntityTrait, IntoActiveModel}; @@ -11,7 +11,7 @@ async fn function_handler(event: Request) -> Result, Error> { dotenv().ok(); // Extract and serialize the data. - let wrapper = match parse_request_body::(&event) { + let wrapper = match parse_request_body::(&event) { Ok(value) => value, Err(e) => return Ok(e.into()), }; diff --git a/lambdas/src/cities/get-cities-bnas.rs b/lambdas/src/cities/get-cities-bnas.rs index 5c672d2..f560aaa 100644 --- a/lambdas/src/cities/get-cities-bnas.rs +++ b/lambdas/src/cities/get-cities-bnas.rs @@ -28,15 +28,16 @@ async fn function_handler(event: Request) -> Result, Error> { // match id { Some(id) => { - let model = city::Entity::find_by_id(id) - .find_also_related(summary::Entity) + let select = city::Entity::find_by_id(id).find_also_related(summary::Entity); + let model = select + .clone() .paginate(&db, page_size) .fetch_page(page - 1) .await?; if model.is_empty() { return Ok(entry_not_found(&event).into()); } - let total_items = city::Entity::find().count(&db).await?; + let total_items = select.count(&db).await?; build_paginated_response(json!(model), total_items, page, page_size, &event) } None => Ok(missing_parameter(&event, "id").into()), diff --git a/lambdas/src/cities/get-cities-census.rs b/lambdas/src/cities/get-cities-census.rs new file mode 100644 index 0000000..8d6e704 --- /dev/null +++ b/lambdas/src/cities/get-cities-census.rs @@ -0,0 +1,87 @@ +use dotenv::dotenv; +use effortless::api::{entry_not_found, missing_parameter, parse_path_parameter}; +use entity::{census, city}; +use lambda_http::{run, service_fn, Body, Error, Request, Response}; +use lambdas::{build_paginated_response, database_connect, pagination_parameters}; +use sea_orm::{prelude::Uuid, EntityTrait, PaginatorTrait}; +use serde_json::json; +use tracing::info; + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + // disable printing the name of the module in every log line. + .with_target(false) + // disabling time is handy because CloudWatch will add the ingestion time. + .without_time() + .init(); + + run(service_fn(function_handler)).await.map_err(|e| { + info!("{e}"); + e + }) +} + +async fn function_handler(event: Request) -> Result, Error> { + dotenv().ok(); + + // Set the database connection. + let db = database_connect(Some("DATABASE_URL_SECRET_ID")).await?; + + // Retrieve pagination parameters if any. + let (page_size, page) = match pagination_parameters(&event) { + Ok((page_size, page)) => (page_size, page), + Err(e) => return Ok(e), + }; + + // Retrieve the ID of the entry to get if any. + let id = match parse_path_parameter::(&event, "id") { + Ok(value) => value, + Err(e) => return Ok(e.into()), + }; + + // + match id { + Some(id) => { + let select = city::Entity::find_by_id(id).find_also_related(census::Entity); + let model = select + .clone() + .paginate(&db, page_size) + .fetch_page(page - 1) + .await?; + if model.is_empty() { + return Ok(entry_not_found(&event).into()); + } + let total_items = select.count(&db).await?; + build_paginated_response(json!(model), total_items, page, page_size, &event) + } + None => Ok(missing_parameter(&event, "id").into()), + } +} + +// #[cfg(test)] +// mod tests { + +// use super::*; +// use aws_lambda_events::http; +// use lambda_http::RequestExt; +// use std::collections::HashMap; + +// #[tokio::test] +// async fn test_handler() { +// let event = http::Request::builder() +// .header(http::header::CONTENT_TYPE, "application/json") +// .body(Body::Empty) +// .expect("failed to build request") +// .with_path_parameters(HashMap::from([( +// "id".to_string(), +// "c49fa94e-542d-421f-9826-e233538be929".to_string(), // Santa Monica, CA +// )])) +// .with_request_context(lambda_http::request::RequestContext::ApiGatewayV2( +// lambda_http::aws_lambda_events::apigw::ApiGatewayV2httpRequestContext::default(), +// )); +// let r = function_handler(event).await.unwrap(); +// dbg!(r); +// } +// } diff --git a/lambdas/src/cities/get-cities.rs b/lambdas/src/cities/get-cities.rs index 78a4cec..0b0ab62 100644 --- a/lambdas/src/cities/get-cities.rs +++ b/lambdas/src/cities/get-cities.rs @@ -36,11 +36,13 @@ async fn function_handler(event: Request) -> Result, Error> { Ok(res) } None => { - let body = city::Entity::find() + let select = city::Entity::find(); + let body = select + .clone() .paginate(&db, page_size) .fetch_page(page - 1) .await?; - let total_items = city::Entity::find().count(&db).await?; + let total_items = select.count(&db).await?; build_paginated_response(json!(body), total_items, page, page_size, &event) } } diff --git a/lambdas/src/lib.rs b/lambdas/src/lib.rs index d36ae02..82a0e7e 100644 --- a/lambdas/src/lib.rs +++ b/lambdas/src/lib.rs @@ -296,7 +296,7 @@ mod tests { use super::*; use aws_lambda_events::http; use effortless::api::{parse_path_parameter, parse_request_body}; - use entity::wrappers::SubmissionPost; + use entity::wrappers::submission::SubmissionPost; use lambda_http::{http::StatusCode, request::from_str, RequestExt}; use std::collections::HashMap;