ordabok/src/db/mod.rs
Lucien Cartier-Tilet c5f5e770e2
All checks were successful
continuous-integration/drone/push Build is passing
Fix word name collision, add two new user-related features
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.
2023-01-18 10:26:45 +01:00

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",
)
})
}
}