begin on associate/unassociate tags function
This commit is contained in:
parent
aad368c70c
commit
629a7629af
19
README.md
19
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
|
||||
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
Reference in New Issue
Block a user