diff --git a/server/Cargo.lock b/server/Cargo.lock index 1594dae3..eb9591e3 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -993,6 +993,7 @@ dependencies = [ "sea-orm", "strum", "thiserror", + "uuid 1.4.1", ] [[package]] @@ -2530,6 +2531,7 @@ dependencies = [ "sea-orm", "serde", "tracing", + "uuid 1.4.1", ] [[package]] diff --git a/server/Cargo.toml b/server/Cargo.toml index 32743bb0..f810691a 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -36,7 +36,7 @@ serde_json = "1.0.107" itertools = "0.11.0" chrono = { version = "0.4.31" } futures = "0.3.28" -uuid = "1.4.1" +uuid = { version = "1.4.1", features = ["v4"] } deriving_via = "1.5.0" reqwest = { version = "0.11.20", default-features = false, features = ["rustls"] } num-traits = "0.2.16" diff --git a/server/domain/src/repository/form_repository.rs b/server/domain/src/repository/form_repository.rs index 04d6c6ce..a8db2cc7 100644 --- a/server/domain/src/repository/form_repository.rs +++ b/server/domain/src/repository/form_repository.rs @@ -21,5 +21,6 @@ pub trait FormRepository: Send + Sync + 'static { form_update_targets: FormUpdateTargets, ) -> Result<(), Error>; async fn post_answer(&self, answers: PostedAnswers) -> Result<(), Error>; + async fn get_all_answers(&self) -> Result, Error>; async fn create_questions(&self, questions: FormQuestionUpdateSchema) -> Result<(), Error>; } diff --git a/server/entrypoint/src/main.rs b/server/entrypoint/src/main.rs index 8fef81f1..730dde24 100644 --- a/server/entrypoint/src/main.rs +++ b/server/entrypoint/src/main.rs @@ -12,7 +12,7 @@ use presentation::{ auth::auth, form_handler::{ create_form_handler, create_question_handler, delete_form_handler, form_list_handler, - get_form_handler, post_answer_handler, update_form_handler, + get_all_answers, get_form_handler, post_answer_handler, update_form_handler, }, health_check_handler::health_check, }; @@ -71,7 +71,10 @@ async fn main() -> anyhow::Result<()> { .patch(update_form_handler), ) .with_state(shared_repository.to_owned()) - .route("/forms/answers", post(post_answer_handler)) + .route( + "/forms/answers", + post(post_answer_handler).get(get_all_answers), + ) .with_state(shared_repository.to_owned()) .route("/forms/questions", post(create_question_handler)) .with_state(shared_repository.to_owned()) diff --git a/server/errors/Cargo.toml b/server/errors/Cargo.toml index 4e3b9097..b0ba8a25 100644 --- a/server/errors/Cargo.toml +++ b/server/errors/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" sea-orm = { workspace = true } strum = { workspace = true } thiserror = "1.0.48" +uuid = { workspace = true } diff --git a/server/errors/src/infra.rs b/server/errors/src/infra.rs index 9bb536b9..a6219e4b 100644 --- a/server/errors/src/infra.rs +++ b/server/errors/src/infra.rs @@ -7,6 +7,11 @@ pub enum InfraError { #[from] source: sea_orm::error::DbErr, }, + #[error("Uuid Parse Error: {}", .source)] + UuidParse { + #[from] + source: uuid::Error, + }, #[error("Form Not Found: id = {}", .id)] FormNotFound { id: i32 }, #[error("Outgoing Error: {}", .cause)] diff --git a/server/infra/entities/src/answers.rs b/server/infra/entities/src/answers.rs index 9c279c1e..f5f12c37 100644 --- a/server/infra/entities/src/answers.rs +++ b/server/infra/entities/src/answers.rs @@ -7,6 +7,7 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: i32, + pub form_id: i32, #[sea_orm(column_type = "Binary(BlobSize::Blob(Some(16)))")] pub user: Vec, pub title: String, @@ -15,10 +16,24 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { + #[sea_orm( + belongs_to = "super::form_meta_data::Entity", + from = "Column::FormId", + to = "super::form_meta_data::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + FormMetaData, #[sea_orm(has_many = "super::real_answers::Entity")] RealAnswers, } +impl Related for Entity { + fn to() -> RelationDef { + Relation::FormMetaData.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::RealAnswers.def() diff --git a/server/infra/entities/src/form_meta_data.rs b/server/infra/entities/src/form_meta_data.rs index 90bc5524..c9d7157f 100644 --- a/server/infra/entities/src/form_meta_data.rs +++ b/server/infra/entities/src/form_meta_data.rs @@ -15,6 +15,8 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { + #[sea_orm(has_many = "super::answers::Entity")] + Answers, #[sea_orm(has_many = "super::default_answer_titles::Entity")] DefaultAnswerTitles, #[sea_orm(has_many = "super::form_questions::Entity")] @@ -25,6 +27,12 @@ pub enum Relation { ResponsePeriod, } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Answers.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::DefaultAnswerTitles.def() diff --git a/server/infra/resource/Cargo.toml b/server/infra/resource/Cargo.toml index 12c1a6d3..547366fb 100644 --- a/server/infra/resource/Cargo.toml +++ b/server/infra/resource/Cargo.toml @@ -22,3 +22,4 @@ serde = { workspace = true } tracing = { workspace = true } num-traits = { workspace = true } regex = { workspace = true } +uuid = { workspace = true } diff --git a/server/infra/resource/src/database/components.rs b/server/infra/resource/src/database/components.rs index db92540c..443e4b97 100644 --- a/server/infra/resource/src/database/components.rs +++ b/server/infra/resource/src/database/components.rs @@ -6,7 +6,7 @@ use domain::form::models::{ use errors::infra::InfraError; use mockall::automock; -use crate::dto::FormDto; +use crate::dto::{FormDto, PostedAnswersDto}; #[async_trait] pub trait DatabaseComponents: Send + Sync { @@ -34,6 +34,7 @@ pub trait FormDatabase: Send + Sync { form_update_targets: FormUpdateTargets, ) -> Result<(), InfraError>; async fn post_answer(&self, answer: PostedAnswers) -> Result<(), InfraError>; + async fn get_all_answers(&self) -> Result, InfraError>; async fn create_questions(&self, questions: FormQuestionUpdateSchema) -> Result<(), InfraError>; } diff --git a/server/infra/resource/src/database/form.rs b/server/infra/resource/src/database/form.rs index 562453a0..fbbf44fb 100644 --- a/server/infra/resource/src/database/form.rs +++ b/server/infra/resource/src/database/form.rs @@ -7,7 +7,8 @@ use domain::form::models::{ use entities::{ answers, default_answer_titles, form_choices, form_meta_data, form_questions, form_webhooks, prelude::{ - DefaultAnswerTitles, FormChoices, FormMetaData, FormQuestions, FormWebhooks, RealAnswers, + Answers, DefaultAnswerTitles, FormChoices, FormMetaData, FormQuestions, FormWebhooks, + RealAnswers, }, real_answers, response_period, sea_orm_active_enums::QuestionType, @@ -18,6 +19,7 @@ use itertools::Itertools; use num_traits::cast::FromPrimitive; use regex::Regex; use sea_orm::{ + prelude::Uuid, sea_query::{Expr, SimpleExpr}, ActiveEnum, ActiveModelTrait, ActiveValue, ActiveValue::Set, @@ -26,7 +28,7 @@ use sea_orm::{ use crate::{ database::{components::FormDatabase, connection::ConnectionPool}, - dto::{FormDto, QuestionDto}, + dto::{AnswerDto, FormDto, PostedAnswersDto, QuestionDto}, }; #[async_trait] @@ -405,6 +407,7 @@ impl FormDatabase for ConnectionPool { let id = answers::ActiveModel { id: Default::default(), + form_id: Set(answer.form_id.to_owned()), user: Set(answer.uuid.to_owned().as_ref().to_vec()), title: Set(embed_title), time_stamp: Set(Utc::now()), @@ -431,6 +434,45 @@ impl FormDatabase for ConnectionPool { Ok(()) } + async fn get_all_answers(&self) -> Result, InfraError> { + stream::iter( + Answers::find() + .order_by_desc(answers::Column::TimeStamp) + .all(&self.pool) + .await?, + ) + .then(|answer| async move { + let answers = RealAnswers::find() + .filter(Expr::col(real_answers::Column::AnswerId).eq(answer.id)) + .all(&self.pool) + .await? + .into_iter() + .map( + |entities::real_answers::Model { + question_id, + answer, + .. + }| AnswerDto { + question_id, + answer, + }, + ) + .collect_vec(); + + Ok(PostedAnswersDto { + uuid: Uuid::from_slice(answer.user.as_slice())?, + timestamp: answer.time_stamp, + form_id: answer.form_id, + title: Some(answer.title), + answers, + }) + }) + .collect::>>() + .await + .into_iter() + .collect::, _>>() + } + async fn create_questions( &self, form_question_update_schema: FormQuestionUpdateSchema, diff --git a/server/infra/resource/src/dto.rs b/server/infra/resource/src/dto.rs index f441303e..3c89ffcd 100644 --- a/server/infra/resource/src/dto.rs +++ b/server/infra/resource/src/dto.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; use domain::form::models::{FormSettings, ResponsePeriod}; +use uuid::Uuid; pub struct QuestionDto { pub id: i32, @@ -79,3 +80,57 @@ impl TryFrom for domain::form::models::Form { .build()) } } + +pub struct AnswerDto { + pub question_id: i32, + pub answer: String, +} + +impl TryFrom for domain::form::models::Answer { + type Error = errors::domain::DomainError; + + fn try_from( + AnswerDto { + question_id, + answer, + }: AnswerDto, + ) -> Result { + Ok(domain::form::models::Answer { + question_id: question_id.into(), + answer, + }) + } +} + +pub struct PostedAnswersDto { + pub uuid: Uuid, + pub timestamp: DateTime, + pub form_id: i32, + pub title: Option, + pub answers: Vec, +} + +impl TryFrom for domain::form::models::PostedAnswers { + type Error = errors::domain::DomainError; + + fn try_from( + PostedAnswersDto { + uuid, + timestamp, + form_id, + title, + answers, + }: PostedAnswersDto, + ) -> Result { + Ok(domain::form::models::PostedAnswers { + uuid, + timestamp, + form_id: form_id.into(), + title: title.into(), + answers: answers + .into_iter() + .map(|answer| answer.try_into()) + .collect::, _>>()?, + }) + } +} diff --git a/server/infra/resource/src/repository/form_repository_impl.rs b/server/infra/resource/src/repository/form_repository_impl.rs index 9af10c10..8f5aa1cb 100644 --- a/server/infra/resource/src/repository/form_repository_impl.rs +++ b/server/infra/resource/src/repository/form_repository_impl.rs @@ -7,6 +7,7 @@ use domain::{ repository::form_repository::FormRepository, }; use errors::Error; +use futures::{stream, stream::StreamExt}; use outgoing::form_outgoing; use crate::{ @@ -77,6 +78,16 @@ impl FormRepository for Repository .map_err(Into::into) } + #[tracing::instrument(skip(self))] + async fn get_all_answers(&self) -> Result, Error> { + stream::iter(self.client.form().get_all_answers().await?) + .then(|posted_answers_dto| async { Ok(posted_answers_dto.try_into()?) }) + .collect::>>() + .await + .into_iter() + .collect::, _>>() + } + async fn create_questions(&self, questions: FormQuestionUpdateSchema) -> Result<(), Error> { self.client .form() diff --git a/server/migration/src/m20230811_062425_create_answer_tables.rs b/server/migration/src/m20230811_062425_create_answer_tables.rs index 05eca048..e74c548c 100644 --- a/server/migration/src/m20230811_062425_create_answer_tables.rs +++ b/server/migration/src/m20230811_062425_create_answer_tables.rs @@ -1,6 +1,9 @@ use sea_orm_migration::prelude::*; -use crate::m20221211_211233_form_questions::FormQuestionsTable; +use crate::{ + m20220101_000001_create_table::FormMetaDataTable, + m20221211_211233_form_questions::FormQuestionsTable, +}; #[derive(DeriveMigrationName)] pub struct Migration; @@ -20,6 +23,13 @@ impl MigrationTrait for Migration { .auto_increment() .primary_key(), ) + .col(ColumnDef::new(AnswersTable::FormId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-form-id-from-answers") + .from(AnswersTable::Answers, AnswersTable::FormId) + .to(FormMetaDataTable::FormMetaData, FormMetaDataTable::Id), + ) .col(ColumnDef::new(AnswersTable::User).uuid().not_null()) .col( ColumnDef::new(AnswersTable::Title) @@ -98,6 +108,7 @@ impl MigrationTrait for Migration { enum AnswersTable { Answers, Id, + FormId, User, Title, TimeStamp, diff --git a/server/presentation/src/form_handler.rs b/server/presentation/src/form_handler.rs index 17fce1fc..f058db93 100644 --- a/server/presentation/src/form_handler.rs +++ b/server/presentation/src/form_handler.rs @@ -110,6 +110,19 @@ pub async fn update_form_handler( } } +pub async fn get_all_answers( + State(repository): State, +) -> impl IntoResponse { + let form_use_case = FormUseCase { + repository: repository.form_repository(), + }; + + match form_use_case.get_all_answers().await { + Ok(answers) => (StatusCode::OK, Json(answers)).into_response(), + Err(err) => handle_error(err).into_response(), + } +} + pub async fn post_answer_handler( State(repository): State, Json(answers): Json, diff --git a/server/usecase/src/form.rs b/server/usecase/src/form.rs index 78169c04..bd03d29b 100644 --- a/server/usecase/src/form.rs +++ b/server/usecase/src/form.rs @@ -44,6 +44,10 @@ impl FormUseCase<'_, R> { self.repository.post_answer(answers).await } + pub async fn get_all_answers(&self) -> Result, Error> { + self.repository.get_all_answers().await + } + pub async fn create_questions(&self, questions: FormQuestionUpdateSchema) -> Result<(), Error> { self.repository.create_questions(questions).await }