diff --git a/README.md b/README.md index 4ab73dc..6ed4d24 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,13 @@ I'll do my best to keep these contents up-to-date as changes are made to the sou First install Rust. I suggest using the [rustup toolchain](https://rustup.rs/) #### Database -A postgres database is required. At this time, the database must be setup and run manually (using docker is recommended). Create a database within postgres called `questionanswer` and choose a password. Set this password in `.cargo/config.toml` +A postgres database is required. At this time, the database must be setup and run manually. -It is convient to run postgres in docker with the postgres:latest docker image +It is convient to run postgres in docker with the postgres:latest docker image. This can be done in a few easy clicks on docker desktop, or as follows with the docker CLI (be sure to pick a password and update [.cargo/config.toml](.cargo/config.toml)) +``` +docker pull postgres:latest +docker run --name my-postgres-container -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=mypassword -e -p 5432:5432 -d postgres:latest +``` #### Build and Run ``` @@ -37,7 +41,9 @@ Axum is the rust web framework doing much of the heavy lifting of this applicati #### [Tokio](https://crates.io/crates/tokio) The tokio framework allows rust to operate asynchoronously. By axums asychonous web nature, it is required by axum. #### [Serde/serde_json](https://crates.io/crates/serde) -A useful package for serializing and deserializing data (specifically json), used in tandem with file operations. +A useful package for serializing and deserializing data. +#### [sqlx](https://crates.io/crates/sqlx) +Manages connections and queries to our postgres database ### Source overview @@ -58,14 +64,9 @@ This is the definition of our `struct Question`. An interesting potential for co To address this, I seperated the Question 'entity' from the Question 'DTO' (data transfer object) from an ORM philosophy. The result is that the Question entity does not contain a direct reference to the question ID, as it is assumed while we are in program space, the store's hashmap will manage this. `to_entity` and `to_dto` function implementions have been provided to make this easier to work with. #### `src/store.rs` The store is responsible for the management of the questions. It does this by -* Loading the questions from the `questions.json` (if it exists, or creating it if not) -* Creating an in-memory hashmap of the jokes * Providing public functions to create, retrieve, update or delete questions -* Writing any mutating changes out to `questions.json` * Handling possible unpermitted operations by returning errors * Handling possible file or I/O errors with some sense of grace before panicing -### `questions.json` -A test dataset of questions. Generated by [ChatGPT](https://chat.openai.com/) ### `question_tag.rs` ### `tag.rs` ### `answer.rs` @@ -80,8 +81,6 @@ These are a few things still to be added * Coded API tests with mock data * Specific defined Error types for common errors #### Lesser priority -* Optimize flush/file writing: Write out the JSON in a pretty structure and avoid re-writing the whole file in cases when it can be avoid (like adding 1 item) -* Use a database alltogether instead of a json file for persistance * Serve basic web page(s) to utilize all APIs * Optimize the put/update handler to support path or body identification, and to update only individual fields passed. * Host application on my raspberry pi on the internet diff --git a/src/pg_store.rs b/src/pg_store.rs index 9e6e2b8..554f79a 100644 --- a/src/pg_store.rs +++ b/src/pg_store.rs @@ -1,4 +1,7 @@ +use std::borrow::Borrow; + use answer::NewAnswer; +use question::QuestionDTO; /// Store is responsible for manageing the in-memory hashmap of questions by providing initialization read/write functions, /// and file I/O operations to persist these questions @@ -73,12 +76,10 @@ impl Store { } // Add new tags to tags table, only if tags with existing label do not exist - pub async fn _add_tags(&mut self, tag_labels: Vec) -> Result, String> { - let insert_values: Vec = tag_labels - .iter() - .map(|label| format!("({}),", label)) - .collect(); - let query = "INSERT INTO tags (label) VALUES $1 ON CONFLICT (label) DO NOTHING RETURNING *" + pub async fn add_tags(&mut self, tag_labels: Vec) -> Result, String> { + let insert_values = tag_labels.join(","); + let query = + "INSERT INTO tags (label) VALUES ($1) ON CONFLICT (label) DO NOTHING RETURNING *"; let result = sqlx::query(query) .bind(insert_values) .fetch_all(&self.connection) @@ -92,6 +93,53 @@ impl Store { } } + //Takes a question id and list of tags, and creates question_tag join associations + //Ignores if association already exists + //Returns list of the question tag associations + pub async fn associate_tags( + &mut self, + question_id: u8, + tags: Vec, + ) -> Result, String> { + let question_id_tag_id_tuple_string = tags + .iter() + .map(|tag| format!("({},{})", question_id.to_string(), tag.id.to_string())) + .collect::>() + .join(","); + let query = "INSERT INTO question_tag VALUES ($1) ON CONFLICT (question_id, tag_id) DO NOTHING RETURNING *"; + let result = sqlx::query(&query) + .bind(question_id_tag_id_tuple_string) + .fetch_all(&self.connection) + .await; + match result { + Ok(pg_rows) => { + let question_tags: Vec = pg_rows + .iter() + .map(|pg_row| { + QuestionTag::new( + Store::id_to_u8(pg_row, "question_id"), + Store::id_to_u8(pg_row, "tag_id"), + ) + }) + .collect(); + Ok(question_tags) + } + Err(e) => Err(e.to_string()), + } + } + + // Takes a question id and a list of tags, and remove any existing question_tag join associations + // Ignores if an association does not already exist + // Returns Ok(true) on success + pub async fn unassociate_tags(&mut self, question_id: u8, tags: Vec) { + let question_id_tag_id_tuple_string = tags + .iter() + .map(|tag| format!("({},{})", question_id.to_string(), tag.id.to_string())) + .collect::>() + .join(","); + let query = "DELETE FROM question_tag WHERE (question_id, tag_id) IN ($1)"; + } + // TODO: property should be a strong type pub async fn fetch_tags_by_property( &self, @@ -115,15 +163,27 @@ impl Store { } } - pub async fn add_question(&mut self, new_question: NewQuestion) -> Result { - let result: Result = sqlx::query("INSERT INTO questions (title, content, tags) VALUES ($1, $2, $3) RETURNING id, title, content, tags") - .bind(new_question.title).bind(new_question.content).bind(new_question.tags).fetch_one(&self.connection).await; - match result { - Ok(pg_row) => Ok(Question::new( - Store::id_to_u8(&pg_row, "id"), - pg_row.get("title"), - pg_row.get("content"), - )), + pub async fn add_question(&mut self, new_question: NewQuestion) -> Result { + let insert_question_result: Result = sqlx::query( + "INSERT INTO questions (title, content) VALUES ($1, $2) RETURNING id, title, content", + ) + .bind(new_question.title) + .bind(new_question.content) + .fetch_one(&self.connection) + .await; + match insert_question_result { + Ok(pg_row) => { + let inserted_question = Question::new( + Store::id_to_u8(&pg_row, "id"), + pg_row.get("title"), + pg_row.get("content"), + ); + let new_tags = &self.add_tags(new_question.tags).await; + match new_tags { + Ok(tags) => Ok(QuestionDTO::new(inserted_question, tags.to_vec())), + Err(e) => Err(e.to_string()), + } + } Err(e) => Err(e.to_string()), } } diff --git a/src/question.rs b/src/question.rs index b3771ec..33e605c 100644 --- a/src/question.rs +++ b/src/question.rs @@ -7,7 +7,7 @@ use crate::*; pub struct NewQuestion { pub title: String, pub content: String, - pub tags: Option>, + pub tags: Vec, } #[derive(Deserialize, Serialize, Clone, Debug)]