fix add/fetch all questions
This commit is contained in:
parent
036f4c4779
commit
094496961a
@ -15,7 +15,9 @@ CREATE TABLE IF NOT EXISTS tags (
|
|||||||
CREATE TABLE IF NOT EXISTS question_tag (
|
CREATE TABLE IF NOT EXISTS question_tag (
|
||||||
question_id INT REFERENCES questions(id),
|
question_id INT REFERENCES questions(id),
|
||||||
tag_id INT REFERENCES tags(id),
|
tag_id INT REFERENCES tags(id),
|
||||||
PRIMARY KEY (question_id, tag_id)
|
PRIMARY KEY (question_id, tag_id),
|
||||||
|
FOREIGN KEY (question_id) REFERENCES questions(id),
|
||||||
|
FOREIGN KEY (tag_id) REFERENCES tags(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create answers table
|
-- Create answers table
|
||||||
|
32
src/api.rs
32
src/api.rs
@ -5,8 +5,9 @@ use self::{
|
|||||||
question::{NewQuestion, QuestionDTO},
|
question::{NewQuestion, QuestionDTO},
|
||||||
};
|
};
|
||||||
use crate::*;
|
use crate::*;
|
||||||
const DEFAULT_PAGE: usize = 0;
|
// sqlx wants 'bigint' = i32
|
||||||
const DEFAULT_PAGE_SIZE: usize = 10;
|
const DEFAULT_PAGE: i32 = 0;
|
||||||
|
const DEFAULT_PAGE_SIZE: i32 = 10;
|
||||||
|
|
||||||
/// Fetches a single question using a provided id. First we fetch the question, then the questions tags, then build that into a response DTO
|
/// Fetches a single question using a provided id. First we fetch the question, then the questions tags, then build that into a response DTO
|
||||||
/// # Parameters
|
/// # Parameters
|
||||||
@ -43,24 +44,29 @@ pub async fn read_questions(
|
|||||||
State(store): State<Arc<RwLock<Store>>>,
|
State(store): State<Arc<RwLock<Store>>>,
|
||||||
Query(pagination): Query<Pagination>,
|
Query(pagination): Query<Pagination>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let page: usize = pagination.page.unwrap_or(DEFAULT_PAGE);
|
let page: i32 = pagination.page.unwrap_or(DEFAULT_PAGE);
|
||||||
let size: usize = pagination.size.unwrap_or(DEFAULT_PAGE_SIZE);
|
let size: i32 = pagination.size.unwrap_or(DEFAULT_PAGE_SIZE);
|
||||||
let start: usize = page * size;
|
let start: i32 = page * size;
|
||||||
let questions_option = store.read().await.fetch_many_questions(start, size).await;
|
let questions_option = store.read().await.fetch_many_questions(start, size).await;
|
||||||
match questions_option {
|
match questions_option {
|
||||||
Some(questions) => {
|
Some(questions) => {
|
||||||
|
println!("Num question {}", questions.len());
|
||||||
let mut response_vec_dto: Vec<QuestionDTO> = vec![];
|
let mut response_vec_dto: Vec<QuestionDTO> = vec![];
|
||||||
for question in questions {
|
for question in questions {
|
||||||
//Not ideal - hitting the database serially for the tags of each invidual question. Can be optimized with more complex sql
|
//Not ideal - hitting the database serially for the tags of each invidual question. Can be optimized with more complex sql
|
||||||
let tags = store
|
let tags_option = store.read().await.get_tags_for_question(question.id).await;
|
||||||
.read()
|
match tags_option {
|
||||||
.await
|
Some(tags) => {
|
||||||
.fetch_tags_by_property(question.id, "question_id")
|
println!("tags {:?}", tags);
|
||||||
.await
|
|
||||||
.unwrap_or(vec![]);
|
|
||||||
let question_dto: QuestionDTO = QuestionDTO::new(question, tags);
|
let question_dto: QuestionDTO = QuestionDTO::new(question, tags);
|
||||||
response_vec_dto.push(question_dto)
|
response_vec_dto.push(question_dto)
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
let question_dto: QuestionDTO = QuestionDTO::new(question, vec![]);
|
||||||
|
response_vec_dto.push(question_dto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
(StatusCode::OK, Json(response_vec_dto)).into_response()
|
(StatusCode::OK, Json(response_vec_dto)).into_response()
|
||||||
}
|
}
|
||||||
None => StatusCode::NO_CONTENT.into_response(),
|
None => StatusCode::NO_CONTENT.into_response(),
|
||||||
@ -179,6 +185,6 @@ pub async fn create_answer(
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Pagination {
|
pub struct Pagination {
|
||||||
page: Option<usize>,
|
page: Option<i32>,
|
||||||
size: Option<usize>,
|
size: Option<i32>,
|
||||||
}
|
}
|
||||||
|
106
src/pg_store.rs
106
src/pg_store.rs
@ -29,7 +29,7 @@ impl Store {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(pool) => pool,
|
Ok(pool) => pool,
|
||||||
Err(_) => panic!("Could not estable database connection"),
|
Err(_) => panic!("Could not establish database connection"),
|
||||||
};
|
};
|
||||||
sqlx::migrate!().run(&db_pool).await.unwrap();
|
sqlx::migrate!().run(&db_pool).await.unwrap();
|
||||||
Store {
|
Store {
|
||||||
@ -39,9 +39,12 @@ impl Store {
|
|||||||
|
|
||||||
//Get the question tags for a particular question
|
//Get the question tags for a particular question
|
||||||
pub async fn get_tags_for_question(&self, question_id: u8) -> Option<Vec<Tag>> {
|
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 query = "SELECT tags.id, tags.label FROM tags
|
||||||
|
JOIN question_tag on tags.id = question_tag.tag_id
|
||||||
|
JOIN questions on question_tag.question_id = questions.id
|
||||||
|
WHERE questions.id = ($1)";
|
||||||
let result = sqlx::query(query)
|
let result = sqlx::query(query)
|
||||||
.bind(question_id.to_string())
|
.bind(i32::from(question_id))
|
||||||
.fetch_all(&self.connection)
|
.fetch_all(&self.connection)
|
||||||
.await;
|
.await;
|
||||||
match result {
|
match result {
|
||||||
@ -52,7 +55,10 @@ impl Store {
|
|||||||
.collect::<Vec<Tag>>();
|
.collect::<Vec<Tag>>();
|
||||||
Some(tags)
|
Some(tags)
|
||||||
}
|
}
|
||||||
_ => None,
|
Err(e) => {
|
||||||
|
println!("{}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,47 +103,68 @@ impl Store {
|
|||||||
// Add new tags to tags table, only if tags with existing label do not exist. If they already exist, just ignore that
|
// 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
|
// Returns list of tags
|
||||||
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 = tag_labels.join(",");
|
let insert_query = "
|
||||||
let query =
|
INSERT INTO tags (label)
|
||||||
"INSERT INTO tags (label) VALUES ($1) ON CONFLICT (label) DO NOTHING RETURNING *";
|
SELECT * FROM UNNEST(($1)::text[]) AS label
|
||||||
let result = sqlx::query(query)
|
ON CONFLICT (label) DO NOTHING
|
||||||
.bind(insert_values)
|
";
|
||||||
.fetch_all(&self.connection)
|
|
||||||
|
//First run the insert query on the new labels
|
||||||
|
let insert_result = sqlx::query(insert_query)
|
||||||
|
.bind(&tag_labels)
|
||||||
|
.execute(&self.connection)
|
||||||
.await;
|
.await;
|
||||||
match result {
|
match insert_result {
|
||||||
|
Ok(_) => {
|
||||||
|
//Then run the select query on the new labels (which may include already existing labels)
|
||||||
|
let select_query = "
|
||||||
|
SELECT id, label FROM tags
|
||||||
|
WHERE label = ANY($1::text[]);
|
||||||
|
";
|
||||||
|
match sqlx::query(select_query)
|
||||||
|
.bind(&tag_labels)
|
||||||
|
.fetch_all(&self.connection)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(pg_rows) => {
|
Ok(pg_rows) => {
|
||||||
let tags: Vec<Tag> = pg_rows
|
let tags: Vec<Tag> = pg_rows
|
||||||
.iter()
|
.iter()
|
||||||
.map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")))
|
.map(|pg_row| {
|
||||||
|
Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label"))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(tags)
|
Ok(tags)
|
||||||
}
|
}
|
||||||
Err(e) => Err(e.to_string()),
|
Err(e) => Err(e.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => Err(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Takes a question id and list of tags, and creates question_tag join associations
|
/// Takes a question id and list of tags, and creates question_tag join associations
|
||||||
//Ignores if association already exists
|
/// Ignores if association already exists
|
||||||
//Returns list of the question tag associations
|
/// Returns list of the question tag associations
|
||||||
pub async fn associate_tags(
|
pub async fn associate_tags(
|
||||||
&mut self,
|
&mut self,
|
||||||
question_id: u8,
|
question_id: u8,
|
||||||
tags: &[Tag],
|
tags: &[Tag],
|
||||||
) -> Result<Vec<QuestionTag>, String> {
|
) -> 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 tag_ids_string: Vec<String> = tags.iter().map(|tag| tag.id.to_string()).collect();
|
||||||
let question_id_tag_id_tuple_string = tags
|
|
||||||
.iter()
|
|
||||||
.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 query = "
|
||||||
|
INSERT INTO question_tag (question_id, tag_id)
|
||||||
|
SELECT $1::smallint, UNNEST($2::smallint[])
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
";
|
||||||
let result = sqlx::query(query)
|
let result = sqlx::query(query)
|
||||||
.bind(question_id_tag_id_tuple_string)
|
.bind(question_id.to_string())
|
||||||
|
.bind(tag_ids_string)
|
||||||
.fetch_all(&self.connection)
|
.fetch_all(&self.connection)
|
||||||
.await;
|
.await;
|
||||||
match result {
|
match result {
|
||||||
Ok(pg_rows) => {
|
Ok(pg_rows) => {
|
||||||
|
println!("Num rows {}", pg_rows.len());
|
||||||
let question_tags: Vec<QuestionTag> = pg_rows
|
let question_tags: Vec<QuestionTag> = pg_rows
|
||||||
.iter()
|
.iter()
|
||||||
.map(|pg_row| {
|
.map(|pg_row| {
|
||||||
@ -178,25 +205,32 @@ impl Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch a list of tags by either the tag id, or by the label. Up to the caller
|
// 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(
|
pub async fn _fetch_tags_by_property(
|
||||||
&self,
|
&self,
|
||||||
propert_id: u8,
|
propert_id: u8,
|
||||||
property_type: &str,
|
property_type: &str,
|
||||||
) -> Option<Vec<Tag>> {
|
) -> Option<Vec<Tag>> {
|
||||||
let result = sqlx::query("SELECT * FROM tags WHERE $1 = $2")
|
println!("Property type {}", property_type);
|
||||||
.bind(property_type)
|
println!("Property id {} ", propert_id);
|
||||||
|
let query = format!("SELECT * FROM tags WHERE {} = ($2);", property_type).to_string(); //looks risky, but user does not get to control property type
|
||||||
|
let result = sqlx::query(&query)
|
||||||
|
.bind(property_type.to_string())
|
||||||
.bind(propert_id.to_string())
|
.bind(propert_id.to_string())
|
||||||
.fetch_all(&self.connection)
|
.fetch_all(&self.connection)
|
||||||
.await;
|
.await;
|
||||||
match result {
|
match result {
|
||||||
Ok(tag_rows) => {
|
Ok(pg_rows) => {
|
||||||
let tags: Vec<Tag> = tag_rows
|
println!("num tag rows {}", pg_rows.len());
|
||||||
|
let tags: Vec<Tag> = pg_rows
|
||||||
.iter()
|
.iter()
|
||||||
.map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")))
|
.map(|pg_row| Tag::new(Store::id_to_u8(pg_row, "id"), pg_row.get("label")))
|
||||||
.collect();
|
.collect();
|
||||||
Some(tags)
|
Some(tags)
|
||||||
}
|
}
|
||||||
_ => None,
|
Err(e) => {
|
||||||
|
println!("err {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,15 +326,17 @@ impl Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch many questions - do not worry about joining the tags
|
// Fetch many questions - do not worry about joining the tags
|
||||||
pub async fn fetch_many_questions(&self, start: usize, size: usize) -> Option<Vec<Question>> {
|
pub async fn fetch_many_questions(&self, start: i32, size: i32) -> Option<Vec<Question>> {
|
||||||
let rows_result: Result<Vec<PgRow>, sqlx::Error> =
|
let rows_result: Result<Vec<PgRow>, sqlx::Error> =
|
||||||
sqlx::query("SELECT * FROM questions ORDER BY id LIMIT $1 OFFSET $2")
|
sqlx::query("SELECT * FROM questions ORDER BY id LIMIT $1 OFFSET $2")
|
||||||
.bind(size.to_string())
|
.bind(size)
|
||||||
.bind(start.to_string())
|
.bind(start)
|
||||||
.fetch_all(&self.connection)
|
.fetch_all(&self.connection)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match rows_result {
|
match rows_result {
|
||||||
Ok(pg_rows) => {
|
Ok(pg_rows) => {
|
||||||
|
println!("num rows {}", pg_rows.len());
|
||||||
let mut result: Vec<Question> = vec![];
|
let mut result: Vec<Question> = vec![];
|
||||||
for pg_row in pg_rows {
|
for pg_row in pg_rows {
|
||||||
result.push(Question::new(
|
result.push(Question::new(
|
||||||
@ -311,7 +347,10 @@ impl Store {
|
|||||||
}
|
}
|
||||||
Some(result)
|
Some(result)
|
||||||
}
|
}
|
||||||
_ => None,
|
Err(e) => {
|
||||||
|
println!("{}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,6 +388,7 @@ impl Store {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add an answer entity
|
||||||
pub async fn add_answer(&mut self, new_answer: NewAnswer) -> Result<Answer, String> {
|
pub async fn add_answer(&mut self, new_answer: NewAnswer) -> Result<Answer, String> {
|
||||||
let result =
|
let result =
|
||||||
sqlx::query("INSERT INTO answers VALUES ($1,$2) RETURNING id, content, question_id")
|
sqlx::query("INSERT INTO answers VALUES ($1,$2) RETURNING id, content, question_id")
|
||||||
|
Reference in New Issue
Block a user