All checks were successful
continuous-integration/drone/push Build is passing
This commit changes the primary key of words to a serial number. That way, two words with the same normalized value will not collide with one another. It also adds two new tables in the database: - Users following languages - Users learning words The former can represent two stages of learning a word: - Either the user is currently learning it - Or they consider they know it and don’t need to work on it anymore These two new tables now have their API query available through the GraphQL API. This commit also fixes the issue of word-related tables and types not being dropped when resetting the database.
274 lines
8.2 KiB
Rust
274 lines
8.2 KiB
Rust
pub mod models;
|
|
pub mod schema;
|
|
|
|
use self::models::languages::Language;
|
|
use self::models::users::User;
|
|
use self::models::words::Word;
|
|
|
|
use diesel::pg::PgConnection;
|
|
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
|
|
use diesel::result::Error;
|
|
use diesel::{insert_into, prelude::*};
|
|
|
|
use dotenvy::dotenv;
|
|
use juniper::{graphql_value, DefaultScalarValue, FieldError, IntoFieldError};
|
|
use std::env;
|
|
use tracing::info;
|
|
|
|
#[derive(Debug)]
|
|
pub struct DatabaseError {
|
|
long: String,
|
|
#[allow(dead_code)]
|
|
short: String,
|
|
}
|
|
|
|
impl DatabaseError {
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
pub fn new<S, T>(long: S, short: T) -> Self
|
|
where
|
|
T: ToString,
|
|
S: ToString,
|
|
{
|
|
Self {
|
|
long: long.to_string(),
|
|
short: short.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for DatabaseError {}
|
|
|
|
impl std::fmt::Display for DatabaseError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.long)
|
|
}
|
|
}
|
|
|
|
impl IntoFieldError for DatabaseError {
|
|
fn into_field_error(self) -> juniper::FieldError<DefaultScalarValue> {
|
|
let short = self.short;
|
|
FieldError::new(self.long, graphql_value!({ "error": short }))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Database {
|
|
conn: Pool<ConnectionManager<PgConnection>>,
|
|
}
|
|
|
|
impl juniper::Context for Database {}
|
|
|
|
impl Default for Database {
|
|
fn default() -> Self {
|
|
Self {
|
|
conn: Database::get_connection_pool(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Database {
|
|
pub fn get_connection_pool() -> Pool<ConnectionManager<PgConnection>> {
|
|
dotenv().ok();
|
|
let database_url =
|
|
env::var("DATABASE_URL").expect("DATABASE_URL must be set!");
|
|
info!("Connecting to {}", database_url);
|
|
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
|
Pool::builder()
|
|
.test_on_check_out(true)
|
|
.build(manager)
|
|
.expect("Could not build connection pool")
|
|
}
|
|
|
|
fn conn(
|
|
&self,
|
|
) -> Result<PooledConnection<ConnectionManager<PgConnection>>, DatabaseError>
|
|
{
|
|
self.conn.get().map_err(|e| {
|
|
DatabaseError::new(
|
|
format!("Failed to connect to database: {e:?}"),
|
|
"Database connection error",
|
|
)
|
|
})
|
|
}
|
|
|
|
pub fn all_languages(&self) -> Result<Vec<Language>, DatabaseError> {
|
|
use self::schema::languages::dsl::languages;
|
|
languages
|
|
.load::<Language>(&mut self.conn()?)
|
|
.map_err(|e| {
|
|
info!("Failed to retrieve languages from database: {e:?}");
|
|
})
|
|
.map_err(|e| {
|
|
DatabaseError::new(
|
|
format!("Failed to retrieve languages: {e:?}"),
|
|
"Failed to retrieve languages",
|
|
)
|
|
})
|
|
}
|
|
|
|
pub fn all_users(&self) -> Result<Vec<User>, DatabaseError> {
|
|
use self::schema::users::dsl::users;
|
|
users.load::<User>(&mut self.conn()?).map_err(|e| {
|
|
DatabaseError::new(
|
|
format!("Failed to retrieve languages: {e:?}"),
|
|
"Failed to retrieve languages",
|
|
)
|
|
})
|
|
}
|
|
|
|
pub fn find_language(
|
|
&self,
|
|
query: &str,
|
|
) -> Result<Vec<Language>, DatabaseError> {
|
|
use self::schema::languages::dsl;
|
|
dsl::languages
|
|
.filter(dsl::name.ilike(format!("%{query}%")))
|
|
.load::<Language>(&mut self.conn()?)
|
|
.map_err(|e| {
|
|
DatabaseError::new(
|
|
format!(
|
|
"Failed to retrieve languages with query {query}: {e:?}"
|
|
),
|
|
"Failed to retrieve languages",
|
|
)
|
|
})
|
|
}
|
|
|
|
pub fn find_user(&self, query: &str) -> Result<Vec<User>, DatabaseError> {
|
|
use self::schema::users::dsl;
|
|
dsl::users
|
|
.filter(dsl::username.ilike(format!("%{query}%")))
|
|
.load::<User>(&mut self.conn()?)
|
|
.map_err(|e| {
|
|
DatabaseError::new(
|
|
format!(
|
|
"Failed to retrieve users with query {query}: {e:?}"
|
|
),
|
|
"Failed to retrieve languages",
|
|
)
|
|
})
|
|
}
|
|
|
|
pub fn language(
|
|
&self,
|
|
name: &str,
|
|
owner: &str,
|
|
) -> Result<Option<Language>, DatabaseError> {
|
|
use self::schema::languages::dsl;
|
|
match dsl::languages
|
|
.filter(dsl::name.eq(name))
|
|
.filter(dsl::owner.eq(owner))
|
|
.first(&mut self.conn()?)
|
|
{
|
|
Ok(val) => Ok(Some(val)),
|
|
Err(Error::NotFound) => Ok(None),
|
|
Err(e) => Err(DatabaseError::new(
|
|
format!(
|
|
"Failed to find language {name} belonging to {owner}: {e:?}"
|
|
),
|
|
"Database error",
|
|
)),
|
|
}
|
|
}
|
|
|
|
pub fn user(&self, id: &str) -> Result<Option<User>, DatabaseError> {
|
|
use self::schema::users::dsl::users;
|
|
match users.find(id).first::<User>(&mut self.conn()?) {
|
|
Ok(val) => Ok(Some(val)),
|
|
Err(Error::NotFound) => Ok(None),
|
|
Err(e) => Err(DatabaseError::new(
|
|
format!("Failed to retrieve user {id} from database: {e:?}"),
|
|
"Database Error",
|
|
)),
|
|
}
|
|
}
|
|
|
|
pub fn insert_user(
|
|
&self,
|
|
username: String,
|
|
id: String,
|
|
) -> Result<User, DatabaseError> {
|
|
use self::schema::users::dsl::users;
|
|
let user = User { id, username };
|
|
match insert_into(users).values(user.clone()).execute(
|
|
&mut self.conn().map_err(|e| {
|
|
DatabaseError::new(
|
|
format!("Failed to connect to the database: {e:?}"),
|
|
"Connection error",
|
|
)
|
|
})?,
|
|
) {
|
|
Ok(_) => Ok(user),
|
|
Err(e) => Err(DatabaseError {
|
|
long: format!("Failed to insert user {user:?}: {e:?}"),
|
|
short: "Data insertion error".to_string(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn delete_user(&self, id: &str) -> Result<(), DatabaseError> {
|
|
use self::schema::users::dsl::users;
|
|
match diesel::delete(users.find(id.to_string()))
|
|
.execute(&mut self.conn()?)
|
|
{
|
|
Ok(_) => Ok(()),
|
|
Err(e) => Err(DatabaseError::new(
|
|
format!("Failed to delete user {id}: {e:?}"),
|
|
"User deletion error",
|
|
)),
|
|
}
|
|
}
|
|
|
|
pub fn word_id(&self, id: uuid::Uuid) -> Result<Option<Word>, DatabaseError> {
|
|
use self::schema::words::dsl;
|
|
match dsl::words.find(id).first::<Word>(&mut self.conn()?) {
|
|
Ok(val) => Ok(Some(val)),
|
|
Err(Error::NotFound) => Ok(None),
|
|
Err(e) => Err(DatabaseError::new(
|
|
format!("Failed to retrieve word {id} from database: {e:?}"),
|
|
"Database Error",
|
|
)),
|
|
}
|
|
}
|
|
|
|
pub fn words(
|
|
&self,
|
|
language: uuid::Uuid,
|
|
word: &str,
|
|
) -> Result<Vec<Word>, DatabaseError> {
|
|
use self::schema::words::dsl;
|
|
dsl::words
|
|
.filter(dsl::language.eq(language))
|
|
.filter(dsl::norm.eq(word))
|
|
.load::<Word>(&mut self.conn()?)
|
|
.map_err(|e| {
|
|
DatabaseError::new(
|
|
format!(
|
|
"Failed to retrieve word {word} from language {language}: {e:?}"
|
|
),
|
|
"Failed to retrieve languages",
|
|
)
|
|
})
|
|
}
|
|
|
|
pub fn find_word(
|
|
&self,
|
|
language: uuid::Uuid,
|
|
query: &str,
|
|
) -> Result<Vec<Word>, DatabaseError> {
|
|
use self::schema::words::dsl;
|
|
dsl::words
|
|
.filter(dsl::language.eq(language))
|
|
.filter(dsl::norm.ilike(format!("%{query}%")))
|
|
.load::<Word>(&mut self.conn()?)
|
|
.map_err(|e| {
|
|
DatabaseError::new(
|
|
format!(
|
|
"Failed to retrieve words from language {language} with query {query}: {e:?}"
|
|
),
|
|
"Failed to retrieve languages",
|
|
)
|
|
})
|
|
}
|
|
}
|