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/)
|
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
|
||||||
|
@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)]
|
||||||
|
Reference in New Issue
Block a user