support more DB functions

This commit is contained in:
David Westgate 2024-06-01 01:57:49 -07:00
parent 629a7629af
commit b6ad349ed7
2 changed files with 116 additions and 63 deletions

View File

@ -18,19 +18,18 @@ pub async fn read_question(
State(store): State<Arc<RwLock<Store>>>,
Path(id): Path<u8>,
) -> Response {
match store.read().await.fetch_one_question_by_id(id).await {
//First, fetch the question
let question_result = store.read().await.fetch_one_question_by_id(id).await;
match question_result {
Some(question) => {
// We could do this in one DB hit with a purpose built store function and different underlying structures, but two hits isn't that bad.
let tags = store
.read()
.await
.fetch_tags_by_property(question.id, "question_id")
.await
.unwrap_or(vec![]);
let response_dto: QuestionDTO = QuestionDTO::new(question, tags);
response_dto.into_response()
//Then fetch the tags for this question, if there are any
let tags_result = store.read().await.get_tags_for_question(question.id).await;
match tags_result {
Some(tags) => QuestionDTO::new(question, tags).into_response(),
None => QuestionDTO::new(question, vec![]).into_response(),
}
None => (StatusCode::NOT_FOUND).into_response(),
}
None => (StatusCode::NOT_EXTENDED).into_response(),
}
}
@ -79,12 +78,6 @@ pub async fn create_question(
State(store): State<Arc<RwLock<Store>>>,
Json(new_question): Json<NewQuestion>,
) -> Response {
//Step 1: See which of supplied tags already exists
//Step 2: Create the non-existing tags
//Step 3: Create the question
//Step 4: Create the join listing
//Step 5: Return result
//Step 6: Ponder the use of ORMs
match store.write().await.add_question(new_question).await {
Ok(question) => (StatusCode::CREATED, Json(&question)).into_response(),
Err(e) => (StatusCode::CONFLICT, e).into_response(),
@ -102,21 +95,6 @@ pub async fn update_question(
State(_store): State<Arc<RwLock<Store>>>,
Json(_question): Json<Question>,
) -> Response {
// Step 0: Update the question entry
// Step 1: Fetch the question_tags for the given tags
// Step 2: Fetch the tags for the given question tags
// Step 3: new_tags_labels = list of tag labels not already on question
// Step 3a: Fetch existing_new_tag as tags which already by given name exist, but do not yet have a question_tag association
// Step 3b: Create question_tag association for 3a
// Step 3c: Create new tags which do not already exist
// Step 3d: Create question_tag association for 3c
// Step 4: remove_tag_labls = list of tag labs which should no longer have a question_tag association with the question
// Step 4a: Fetch existing_old_tags as tags by given na
// match store.write().await.update_question(question) {
// Ok(question) => question.into_response(),
// Err(e) => (StatusCode::NOT_FOUND, e).into_response(),
// }
todo!()
}

View File

@ -1,14 +1,11 @@
use std::borrow::Borrow;
use answer::NewAnswer;
use question::QuestionDTO;
use std::vec;
/// 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
/// TODO - Results returning errors should use specified types, not strings
use self::{
answer::Answer,
question::{NewQuestion, Question},
answer::{Answer, NewAnswer},
question::{NewQuestion, Question, QuestionDTO},
question_tag::QuestionTag,
tag::Tag,
};
@ -22,7 +19,7 @@ pub struct Store {
impl Store {
/// Helper to deal with unwrapping postgres fields as u8 which we want. Not gracefull if you pass in the wrong field name - so do not do that
fn id_to_u8(pg_row: &PgRow, id_name: &str) -> u8 {
pg_row.try_get::<i16, _>(id_name).unwrap() as u8
pg_row.try_get::<i32, _>(id_name).unwrap() as u8
}
pub async fn new(db_url: &str) -> Self {
@ -40,6 +37,26 @@ impl Store {
}
}
//Get the question tags for a particular question
pub async fn get_tags_for_question(&self, question_id: u8) -> Option<Vec<Tag>> {
let query = "SELECT label FROM tag JOIN question_tag on tag.id = question_tag.tag_id JOIN question on tag.question_id = $1";
let result = sqlx::query(query)
.bind(question_id.to_string())
.fetch_all(&self.connection)
.await;
match result {
Ok(pg_rows) => {
let tags: Vec<Tag> = pg_rows
.iter()
.map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")))
.collect::<Vec<Tag>>();
Some(tags)
}
_ => None,
}
}
//Remove a question/tag association
pub async fn _remove_question_tag(
&mut self,
question_id: u8,
@ -55,6 +72,8 @@ impl Store {
Err(e) => Err(e.to_string()),
}
}
//Add a question/tag association
pub async fn _add_question_tag(
&mut self,
question_id: u8,
@ -75,7 +94,8 @@ 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. If they already exist, just ignore that
// Returns list of tags
pub async fn add_tags(&mut self, tag_labels: Vec<String>) -> Result<Vec<Tag>, String> {
let insert_values = tag_labels.join(",");
let query =
@ -85,10 +105,13 @@ impl Store {
.fetch_all(&self.connection)
.await;
match result {
Ok(pg_rows) => Ok(pg_rows
Ok(pg_rows) => {
let tags: Vec<Tag> = pg_rows
.iter()
.map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")))
.collect()),
.collect();
Ok(tags)
}
Err(e) => Err(e.to_string()),
}
}
@ -99,15 +122,17 @@ impl Store {
pub async fn associate_tags(
&mut self,
question_id: u8,
tags: Vec<Tag>,
tags: &[Tag],
) -> Result<Vec<QuestionTag>, String> {
//for the query binding we must convert the tag vector to a comma seperated string list of questionid/tagid tuples
let question_id_tag_id_tuple_string = tags
.iter()
.map(|tag| format!("({},{})", question_id.to_string(), tag.id.to_string()))
.map(|tag| format!("({},{})", question_id, tag.id))
.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)
let result = sqlx::query(query)
.bind(question_id_tag_id_tuple_string)
.fetch_all(&self.connection)
.await;
@ -131,16 +156,28 @@ impl Store {
// 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>) {
pub async fn _unassociate_tags(
&mut self,
question_id: u8,
tags: Vec<Tag>,
) -> Result<bool, String> {
let question_id_tag_id_tuple_string = tags
.iter()
.map(|tag| format!("({},{})", question_id.to_string(), tag.id.to_string()))
.map(|tag| format!("({},{})", question_id, tag.id))
.collect::<Vec<String>>()
.join(",");
let query = "DELETE FROM question_tag WHERE (question_id, tag_id) IN ($1)";
match sqlx::query(query)
.bind(question_id_tag_id_tuple_string)
.execute(&self.connection)
.await
{
Ok(_) => Ok(true),
Err(e) => Err(e.to_string()),
}
}
// TODO: property should be a strong type
// Fetch a list of tags by either the tag id, or by the label. Up to the caller
pub async fn fetch_tags_by_property(
&self,
propert_id: u8,
@ -163,6 +200,7 @@ impl Store {
}
}
//Add a new question - but also create the tags and question/tag associations as needed
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",
@ -171,6 +209,7 @@ impl Store {
.bind(new_question.content)
.fetch_one(&self.connection)
.await;
// first, insert the new question
match insert_question_result {
Ok(pg_row) => {
let inserted_question = Question::new(
@ -178,9 +217,25 @@ impl Store {
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())),
// Then create the new tags (if needed)
let new_tags_result = &self.add_tags(new_question.tags).await;
match new_tags_result {
Ok(new_tags_result) => {
let association = self
.associate_tags(inserted_question.id, new_tags_result)
.await;
// Finally, create the question/tag join
match association {
Ok(_) => {
// Now, return the QuestionDTO
Ok(QuestionDTO::new(
inserted_question,
new_tags_result.to_vec(),
))
}
Err(e) => Err(e.to_string()),
}
}
Err(e) => Err(e.to_string()),
}
}
@ -188,17 +243,39 @@ impl Store {
}
}
// Delete the question tags association for the given question, then delete the question
// Also clean up orphaned tags just in case
pub async fn remove_question(&mut self, id: u8) -> Result<bool, String> {
let result = sqlx::query("DELETE FROM questions WHERE id = $1")
let delete_question_tag_query = "DELETE FROM question_tag WHERE question_id = $1";
let delete_question_query = "DELETE FROM questions WHERE id = $1";
// First, delete any possible question/tag associations
match sqlx::query(delete_question_tag_query)
.bind(id.to_string())
.execute(&self.connection)
.await;
match result {
.await
{
Ok(_) => {
//Now, delete the question
match sqlx::query(delete_question_query)
.bind(id.to_string())
.execute(&self.connection)
.await
{
Ok(_) => {
// Finally remove any tags that may have been orphaned
match self.remove_orphan_tags().await {
Ok(_) => Ok(true),
Err(e) => Err(e.to_string()),
}
}
Err(e) => Err(e.to_string()),
}
}
Err(e) => Err(e.to_string()),
}
}
// Fetch one question, but do not worry about joining the tags
pub async fn fetch_one_question_by_id(&self, id: u8) -> Option<Question> {
let row_result = sqlx::query("SELECT * FROM questions WHERE id = $1")
.bind(id.to_string())
@ -214,6 +291,7 @@ impl Store {
}
}
// Fetch many questions - do not worry about joining the tags
pub async fn fetch_many_questions(&self, start: usize, size: usize) -> Option<Vec<Question>> {
let rows_result: Result<Vec<PgRow>, sqlx::Error> =
sqlx::query("SELECT * FROM questions ORDER BY id LIMIT $1 OFFSET $2")
@ -238,8 +316,8 @@ impl Store {
}
/// Remove tags from tags table, which have no question tag association
/// Returns true if any tags were removed, false otherwise
pub async fn _remove_orphan_tags(&mut self) -> Result<bool, String> {
/// Returns true on success
pub async fn remove_orphan_tags(&mut self) -> Result<bool, String> {
let result = sqlx::query(
"DELETE FROM tags where id NOT IN (SELECT DISTINCT tag_id from question_tag)",
)
@ -251,10 +329,7 @@ impl Store {
}
}
pub fn _sync_question_tags(&mut self, _tags: Vec<Tag>) {
todo!()
}
//Update a question entity - just the question details (not tags)
pub async fn _update_question(&mut self, question: Question) -> Result<Question, String> {
let result = sqlx::query("UPDATE questions SET title = $1 AND SET content = $2 WHERE id = $3 RETURNING id, title, content")
.bind(question.title).bind(question.content).bind(question.id.to_string())