support more DB functions
This commit is contained in:
parent
629a7629af
commit
b6ad349ed7
42
src/api.rs
42
src/api.rs
@ -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!()
|
||||
}
|
||||
|
||||
|
131
src/pg_store.rs
131
src/pg_store.rs
@ -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())
|
||||
|
Reference in New Issue
Block a user