begin on associate/unassociate tags function

This commit is contained in:
David Westgate 2024-05-31 15:15:49 -07:00
parent aad368c70c
commit 629a7629af
3 changed files with 85 additions and 26 deletions

View File

@ -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/) First install Rust. I suggest using the [rustup toolchain](https://rustup.rs/)
#### Database #### 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 #### 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) #### [Tokio](https://crates.io/crates/tokio)
The tokio framework allows rust to operate asynchoronously. By axums asychonous web nature, it is required by axum. 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) #### [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 ### 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. 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` #### `src/store.rs`
The store is responsible for the management of the questions. It does this by 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 * 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 unpermitted operations by returning errors
* Handling possible file or I/O errors with some sense of grace before panicing * 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` ### `question_tag.rs`
### `tag.rs` ### `tag.rs`
### `answer.rs` ### `answer.rs`
@ -80,8 +81,6 @@ These are a few things still to be added
* Coded API tests with mock data * Coded API tests with mock data
* Specific defined Error types for common errors * Specific defined Error types for common errors
#### Lesser priority #### 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 * 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. * 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 * Host application on my raspberry pi on the internet

View File

@ -1,4 +1,7 @@
use std::borrow::Borrow;
use answer::NewAnswer; use answer::NewAnswer;
use question::QuestionDTO;
/// Store is responsible for manageing the in-memory hashmap of questions by providing initialization read/write functions, /// 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 /// 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 // 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<String>) -> Result<Vec<Tag>, String> { pub async fn add_tags(&mut self, tag_labels: Vec<String>) -> Result<Vec<Tag>, String> {
let insert_values: Vec<String> = tag_labels let insert_values = tag_labels.join(",");
.iter() let query =
.map(|label| format!("({}),", label)) "INSERT INTO tags (label) VALUES ($1) ON CONFLICT (label) DO NOTHING RETURNING *";
.collect();
let query = "INSERT INTO tags (label) VALUES $1 ON CONFLICT (label) DO NOTHING RETURNING *"
let result = sqlx::query(query) let result = sqlx::query(query)
.bind(insert_values) .bind(insert_values)
.fetch_all(&self.connection) .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<Tag>,
) -> Result<Vec<QuestionTag>, String> {
let question_id_tag_id_tuple_string = tags
.iter()
.map(|tag| format!("({},{})", question_id.to_string(), tag.id.to_string()))
.collect::<Vec<String>>()
.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<QuestionTag> = 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<Tag>) {
let question_id_tag_id_tuple_string = tags
.iter()
.map(|tag| format!("({},{})", question_id.to_string(), tag.id.to_string()))
.collect::<Vec<String>>()
.join(",");
let query = "DELETE FROM question_tag WHERE (question_id, tag_id) IN ($1)";
}
// TODO: property should be a strong type // TODO: property should be a strong type
pub async fn fetch_tags_by_property( pub async fn fetch_tags_by_property(
&self, &self,
@ -115,15 +163,27 @@ impl Store {
} }
} }
pub async fn add_question(&mut self, new_question: NewQuestion) -> Result<Question, String> { pub async fn add_question(&mut self, new_question: NewQuestion) -> Result<QuestionDTO, String> {
let result: Result<PgRow, sqlx::Error> = sqlx::query("INSERT INTO questions (title, content, tags) VALUES ($1, $2, $3) RETURNING id, title, content, tags") let insert_question_result: Result<PgRow, sqlx::Error> = sqlx::query(
.bind(new_question.title).bind(new_question.content).bind(new_question.tags).fetch_one(&self.connection).await; "INSERT INTO questions (title, content) VALUES ($1, $2) RETURNING id, title, content",
match result { )
Ok(pg_row) => Ok(Question::new( .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"), Store::id_to_u8(&pg_row, "id"),
pg_row.get("title"), pg_row.get("title"),
pg_row.get("content"), 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()), Err(e) => Err(e.to_string()),
} }
} }

View File

@ -7,7 +7,7 @@ use crate::*;
pub struct NewQuestion { pub struct NewQuestion {
pub title: String, pub title: String,
pub content: String, pub content: String,
pub tags: Option<Vec<String>>, pub tags: Vec<String>,
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]