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/)
#### 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

View File

@ -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<String>) -> Result<Vec<Tag>, String> {
let insert_values: Vec<String> = 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<String>) -> Result<Vec<Tag>, 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<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
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<Question, String> {
let result: Result<PgRow, sqlx::Error> = 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<QuestionDTO, String> {
let insert_question_result: Result<PgRow, sqlx::Error> = 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()),
}
}

View File

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