diff --git a/src/db/models/languages.rs b/src/db/models/languages.rs index c93b5b7..8744892 100644 --- a/src/db/models/languages.rs +++ b/src/db/models/languages.rs @@ -27,6 +27,12 @@ pub enum Release { Private, } +impl Default for Release { + fn default() -> Self { + Self::Public + } +} + #[derive( diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, )] @@ -50,6 +56,91 @@ pub enum AgentLanguageRelation { Author, } +#[derive(Default, Debug, Clone, juniper::GraphQLInputObject)] +pub struct NewLanguage { + name: String, + native: Option, + release: Option, + genre: Vec, + abstract_: Option, + description: Option, + rights: Option, + license: Option, +} + +#[derive(Insertable, Debug, Clone)] +#[diesel(table_name = languages)] +struct NewLanguageInternal { + name: String, + native: Option, + release: Release, + genre: Vec, + abstract_: Option, + description: Option, + rights: Option, + license: Option, + owner: String, +} + +impl From for NewLanguageInternal { + fn from(val: NewLanguage) -> Self { + NewLanguageInternal { + name: val.name, + native: val.native, + release: if let Some(release) = val.release { + release + } else { + Release::default() + }, + genre: val.genre, + abstract_: val.abstract_, + description: val.description, + rights: val.rights, + license: val.license, + owner: String::new(), + } + } +} + +impl NewLanguage { + pub fn insert_db( + &self, + db: &Database, + owner: &str, + ) -> Result { + use languages::dsl; + let conn = &mut db.conn()?; + match diesel::insert_into(dsl::languages) + .values(NewLanguageInternal { + owner: owner.to_string(), + ..self.clone().into() + }) + .execute(conn) + { + Ok(_) => dsl::languages + .filter(dsl::name.eq(self.name.clone())) + .filter(dsl::owner.eq(owner)) + .first::(conn) + .map_err(|e| { + DatabaseError::new( + format!( + "Failed to find language {} by user {owner}: {e:?}", + self.name + ), + "Database Error", + ) + }), + Err(e) => Err(DatabaseError::new( + format!( + "Failed to insert language {} by user {owner}: {e:?}", + self.name + ), + "Database Error", + )), + } + } +} + #[derive(Queryable, Insertable, Debug, Clone)] pub struct Language { id: Uuid, @@ -66,6 +157,59 @@ pub struct Language { } impl Language { + pub fn find( + db: &Database, + language: Uuid, + ) -> Result { + use languages::dsl; + dsl::languages.find(language).first::(&mut db.conn()?).map_err(|e| match e { + diesel::NotFound => DatabaseError::new( + format!("Language {language} not found"), + "Not Found" + ), + e => DatabaseError::new( + format!("Error fetching language {language} from database: {e:?}"), + "Database Error" + ) + }) + } + + pub fn delete( + context: &Context, + language_id: Uuid, + ) -> Result<(), DatabaseError> { + use languages::dsl; + let conn = &mut context.db.conn()?; + match dsl::languages.find(language_id) + .first::(conn) + { + Ok(language) if context.user_auth == Some(language.owner.clone()) => { + match diesel::delete(dsl::languages.find(language_id)) + .execute(conn) { + Ok(_) => Ok(()), + Err(e) => Err(DatabaseError::new( + format!("Failed to delete language {language_id}: {e:?}"), + "Database Error" + )) + } + }, + Ok(language) => { + Err(DatabaseError::new( + format!( + "User {} not allowed to delete other user's language {language_id}", + language.owner), + "Unauthorized" + )) + }, + Err(e) => { + Err(DatabaseError::new( + format!("Failed to delete language {language_id}: {e:?}"), + "Database Error" + )) + } + } + } + fn relationship( &self, db: &Database, @@ -290,6 +434,13 @@ pub struct LangTranslatesTo { langto: Uuid, } +#[derive(Insertable)] +#[diesel(table_name = userfollowlanguage)] +pub struct UserFollowLanguageInsert { + pub lang: Uuid, + pub userid: String, +} + #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[diesel(table_name = userfollowlanguage)] pub struct UserFollowLanguage { @@ -297,3 +448,71 @@ pub struct UserFollowLanguage { pub lang: Uuid, pub userid: String, } + +impl UserFollowLanguage { + pub fn user_follow_language( + context: &Context, + userid: &str, + lang: Uuid, + ) -> Result { + let conn = &mut context.db.conn()?; + match languages::dsl::languages.find(lang).first::(conn) { + Err(diesel::NotFound) => Err(DatabaseError::new( + format!("Cannot follow non-existing language {lang}"), + "Invalid Language", + )), + Err(e) => Err(DatabaseError::new( + format!( + "Could not retrieve language {lang} from database: {e:?}" + ), + "Database error", + )), + Ok(language) => { + use userfollowlanguage::dsl; + match diesel::insert_into(dsl::userfollowlanguage) + .values(UserFollowLanguageInsert { lang, userid: userid.to_string() }) + .execute(conn) { + Ok(_) => Ok(language), + Err(e) => Err(DatabaseError::new( + format!("Failed to follow language {lang} as user {userid}: {e:?}"), + "Database Error" + )) + } + } + } + } + + pub fn user_unfollow_language( + context: &Context, + userid: &str, + lang: Uuid, + ) -> Result { + use userfollowlanguage::dsl; + let conn = &mut context.db.conn()?; + match dsl::userfollowlanguage + .filter(dsl::userid.eq(userid.to_string())) + .filter(dsl::lang.eq(lang)) + .first::(conn) { + Ok(relationship) => { + match diesel::delete(dsl::userfollowlanguage.find(relationship.id)) + .execute(conn) { + Ok(_) => Language::find(&context.db, lang), + Err(e) => Err(DatabaseError::new( + format!("Failed to make user {userid} unfollow language {lang}: {e:?}"), + "Database Error" + )) + } + }, + Err(diesel::NotFound) => { + Err(DatabaseError::new( + format!("User {userid} does not follow language {lang}"), + "Invalid", + )) + } + Err(e) => Err(DatabaseError::new( + format!("Failed to retrieve relationship between user {userid} and language {lang} from database: {e:?}"), + "Database Error", + )) + } + } +} diff --git a/src/db/models/words.rs b/src/db/models/words.rs index 6df2087..fb229cb 100644 --- a/src/db/models/words.rs +++ b/src/db/models/words.rs @@ -5,11 +5,11 @@ use crate::{ }; use diesel::prelude::*; use juniper::{FieldResult, GraphQLEnum}; -use schema::{wordrelation, words, wordlearning}; +use schema::{wordlearning, wordrelation, words}; use tracing::info; use uuid::Uuid; -use std::convert::Into; +use std::{convert::Into, str::FromStr}; use super::languages::Language; @@ -20,11 +20,18 @@ pub enum WordRelationship { Related, } -#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, juniper::GraphQLEnum)] +#[derive( + diesel_derive_enum::DbEnum, + Debug, + Clone, + PartialEq, + Eq, + juniper::GraphQLEnum, +)] #[DieselTypePath = "crate::db::schema::sql_types::Wordlearningstatus"] pub enum WordLearningStatus { Learning, - Learned + Learned, } #[derive( @@ -54,6 +61,72 @@ pub enum PartOfSpeech { Other, } +impl Default for PartOfSpeech { + fn default() -> Self { + Self::Noun + } +} + +#[derive(Debug, Clone, juniper::GraphQLInputObject)] +pub struct NewWord { + norm: String, + native: Option, + lemma: Option, + language: String, + partofspeech: PartOfSpeech, + audio: Option, + video: Option, + image: Option, + description: Option, + etymology: Option, + lusage: Option, + morphology: Option, +} + +#[derive(Debug, Clone, Insertable)] +#[diesel(table_name = words)] +struct NewWordInternal { + norm: String, + native: Option, + lemma: Option, + language: Uuid, + partofspeech: PartOfSpeech, + audio: Option, + video: Option, + image: Option, + description: Option, + etymology: Option, + lusage: Option, + morphology: Option, +} + +impl TryFrom for NewWordInternal { + type Error = uuid::Error; + + fn try_from(value: NewWord) -> Result { + let language = Uuid::from_str(&value.language)?; + let lemma = if let Some(original_lemma) = value.lemma { + Some(Uuid::from_str(&original_lemma)?) + } else { + None + }; + Ok(Self { + norm: value.norm, + native: value.native, + lemma, + language, + partofspeech: value.partofspeech, + audio: value.audio, + video: value.video, + image: value.image, + description: value.description, + etymology: value.etymology, + lusage: value.lusage, + morphology: value.morphology, + }) + } +} + #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] pub struct Word { id: Uuid, @@ -229,5 +302,5 @@ pub struct WordLearning { pub id: i32, pub word: Uuid, pub userid: String, - pub status: WordLearningStatus + pub status: WordLearningStatus, } diff --git a/src/graphql/context.rs b/src/graphql/context.rs index 51b60e9..50703fa 100644 --- a/src/graphql/context.rs +++ b/src/graphql/context.rs @@ -27,35 +27,42 @@ impl Default for OtherEnvVar { pub struct Context { pub db: Database, pub appwrite: APVariables, - pub user_auth: bool, + pub user_auth: Option, pub other_vars: OtherEnvVar, } impl Context { - /// HTTP header for a user's session + /// Check if a request is performed by an autentificated user. /// - /// This header `Authorization` must be a single string in the + /// The HTTP header `Authorization` must be a single string in the /// form `userId;userSessionId` with `userId` and `userSessionId` /// being variables given by Appwrite to users that are logged in. - pub async fn user_auth<'r>(&self, auth_token: Option<&'r str>) -> bool { + /// + /// The function returns either the user's ID if the user is + /// authentified or `None`. + pub async fn user_auth<'r>( + &self, + auth_token: Option<&'r str>, + ) -> Option { if let Some(token) = auth_token { let key = token.split(';').collect::>(); if key.len() == 2 { let user_id = key[0]; let session_id = key[1]; match self.appwrite.check_session(session_id, user_id).await { - Ok(val) => val, + Ok(true) => Some(key[0].to_string()), + Ok(false) => None, Err(e) => { info!("Error checking user session: {:?}", e); - false + None } } } else { info!("Invalid session key: {}", token); - false + None } } else { - false + None } } diff --git a/src/graphql/mutation.rs b/src/graphql/mutation.rs index 7273bcf..883e859 100644 --- a/src/graphql/mutation.rs +++ b/src/graphql/mutation.rs @@ -1,6 +1,15 @@ -use juniper::FieldResult; +use std::str::FromStr; -use crate::db::{models::users::User, DatabaseError}; +use juniper::FieldResult; +use uuid::Uuid; + +use crate::db::{ + models::{ + languages::{Language, NewLanguage, UserFollowLanguage}, + users::User, + }, + DatabaseError, +}; use super::Context; @@ -9,7 +18,7 @@ pub struct Mutation; #[juniper::graphql_object(Context = Context)] impl Mutation { fn api_version(context: &Context) -> String { - if context.user_auth { + if context.user_auth.is_some() { "0.1 (authentified)" } else { "0.1 (not authentified)" @@ -49,4 +58,103 @@ impl Mutation { .into()) } } + + pub fn user_follow_language( + context: &Context, + language: String, + ) -> FieldResult { + if let Some(userid) = &context.user_auth { + match Uuid::from_str(&language) { + Err(e) => Err(DatabaseError::new( + format!( + "Could not parse {language} as a valid UUID: {e:?}" + ), + "Bad Request", + ) + .into()), + Ok(lang) => UserFollowLanguage::user_follow_language( + context, + &userid.to_string(), + lang, + ) + .map_err(Into::into), + } + } else { + Err(DatabaseError::new( + "User not authentificated, cannot proceed", + "Unauthorized", + ) + .into()) + } + } + + pub fn user_unfollow_language( + context: &Context, + language: String, + ) -> FieldResult { + if let Some(userid) = &context.user_auth { + match Uuid::from_str(&language) { + Err(e) => Err(DatabaseError::new( + format!( + "Could not parse {language} as a valid UUID: {e:?}" + ), + "Bad Request", + ) + .into()), + Ok(lang) => UserFollowLanguage::user_unfollow_language( + context, + &userid.to_string(), + lang, + ) + .map_err(Into::into), + } + } else { + Err(DatabaseError::new( + "User not authentificated, cannot proceed", + "Unauthorized", + ) + .into()) + } + } + + pub fn new_language( + context: &Context, + language: NewLanguage, + ) -> FieldResult { + if let Some(owner) = &context.user_auth { + language.insert_db(&context.db, owner).map_err(Into::into) + } else { + Err(DatabaseError::new( + "User not authentificated, cannot create new language", + "Unauthorized", + ) + .into()) + } + } + + pub fn delete_language( + context: &Context, + language: String, + ) -> FieldResult> { + if context.user_auth.is_some() { + match Uuid::from_str(&language) { + Ok(uuid) => Language::delete(context, uuid) + .map(|_| None) + .map_err(Into::into), + Err(e) => Err(DatabaseError::new( + format!( + "Could not parse {language} as a valid UUID: {e:?}" + ), + "Bad Request", + ) + .into()), + } + } else { + Err(DatabaseError::new( + "User not authentificated, cannot create new language", + "Unauthorized", + ) + .into()) + } + } }