use crate::{ db::{Database, DatabaseError}, graphql::Context, }; use diesel::prelude::*; use juniper::{FieldResult, GraphQLEnum}; use tracing::info; use uuid::Uuid; use super::super::schema; use super::users::User; use std::convert::Into; use schema::{langandagents, langtranslatesto, languages, userfollowlanguage}; #[derive( diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, )] #[DieselTypePath = "crate::db::schema::sql_types::Release"] pub enum Release { Public, #[graphql(name = "NON_COMMERCIAL")] NonCommercial, Research, Private, } impl Default for Release { fn default() -> Self { Self::Public } } #[derive( diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, )] #[DieselTypePath = "crate::db::schema::sql_types::Dictgenre"] pub enum DictGenre { General, Learning, Etymology, Specialized, Historical, Orthography, Terminology, } #[derive( diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, )] #[DieselTypePath = "crate::db::schema::sql_types::Agentlanguagerelation"] pub enum AgentLanguageRelation { Publisher, 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, name: String, native: Option, release: Release, genre: Vec>, abstract_: Option, created: chrono::NaiveDateTime, description: Option, rights: Option, license: Option, owner: String, } 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, relationship: AgentLanguageRelation, ) -> Result, DatabaseError> { use schema::langandagents::dsl; let conn = &mut db.conn()?; Ok(dsl::langandagents .filter(dsl::language.eq(self.id)) .filter(dsl::relationship.eq(relationship)) .load::(conn) .map_err(|e| { DatabaseError::new( format!("Failed to retrieve language relationship: {e:?}"), "Database reading error", ) })? .iter() .map(|v| { use schema::users::dsl; dsl::users.find(v.agent.clone()).first::(conn) }) .filter_map(|author| match author { Ok(val) => Some(val), Err(e) => { info!("Failed ot retrieve author from database: {:?}", e); None } }) .collect::>()) } } #[juniper::graphql_object(Context = Context)] impl Language { #[graphql(description = "Unique identifier of the language")] fn id(&self) -> String { self.id.to_string() } #[graphql( description = "Name in the main target language (often English) of the described language" )] fn name(&self) -> String { self.name.clone() } #[graphql(description = "Native name of the language")] fn native(&self) -> Option { self.native.clone() } #[graphql(description = "How the dictionary is currently released")] fn release(&self) -> Release { self.release.clone() } #[graphql( name = "targetLanguage", description = "Languages in which the current language is translated" )] fn target_language(&self, context: &Context) -> FieldResult> { use schema::langtranslatesto::dsl; match &mut context.db.conn() { Ok(conn) => Ok(dsl::langtranslatesto .filter(dsl::langfrom.eq(self.id)) .load::(conn) .map_err(|e| { DatabaseError::new( format!( "Failed to retrieve language translations: {e:?}" ), "Database reading failure", ) })? .into_iter() .flat_map(|l| { use schema::languages::dsl; dsl::languages.find(l.langto).first::(conn) }) .collect::>()), Err(e) => Err(DatabaseError::new( format!("Failed to connect to the database: {e:?}"), "Database connection failure", ) .into()), } } #[graphql(description = "What kind of dictionary this is")] fn genre(&self) -> Vec { self.genre.clone().into_iter().flatten().collect() } #[graphql( name = "abstract", description = "Short description of the language" )] fn abstract_(&self) -> Option { self.abstract_.clone() } #[graphql( description = "Time at which the language's dictionary was created" )] fn created(&self) -> String { self.created.to_string() } #[graphql( description = "Longer description of the language, its content can be formatted as Markdown" )] fn description(&self) -> Option { self.description.clone() } #[graphql( description = "Copyrights held by various people over the language's dictionary and its content" )] fn rights(&self) -> Option { self.rights.clone() } #[graphql(description = "License under which the dictionary is released")] fn license(&self) -> Option { self.license.clone() } #[graphql( description = "User with administrative rights over the language" )] fn owner(&self, context: &Context) -> FieldResult { use schema::users::dsl; match &mut context.db.conn() { Ok(conn) => Ok(dsl::users .find(self.owner.clone()) .first::(conn) .map_err(|e| { DatabaseError::new( format!( "Failed to retrieve owner {} of language {}: {e:?}", self.owner, self.name ), "Database error", ) })?), Err(e) => Err(DatabaseError::new( format!("Failed to connect to the database: {e:?}"), "Database connection failure", ) .into()), } } #[graphql( description = "People who participate in the elaboration of the language's dictionary" )] fn authors(&self, context: &Context) -> FieldResult> { self.relationship(&context.db, AgentLanguageRelation::Author) .map_err(Into::into) } #[graphql( description = "People who can and do redistribute the language's dictionary" )] fn publishers(&self, context: &Context) -> FieldResult> { self.relationship(&context.db, AgentLanguageRelation::Publisher) .map_err(Into::into) } #[graphql(description = "People following the language")] fn followers(&self, context: &Context) -> FieldResult> { use schema::userfollowlanguage::dsl; match &mut context.db.conn() { Ok(conn) => { Ok(dsl::userfollowlanguage .filter(dsl::lang.eq(self.id)) .load::(conn) .map_err(|e| { DatabaseError::new(format!("Failed to retrieve language followers for language {}: {e:?}", self.id), "Database error") })? .into_iter() .filter_map(|follow| { use schema::users::dsl; match dsl::users .find(follow.userid.clone()) .first::(conn) { Ok(user) => Some(user), Err(e) => { info!("Failed to retrieve user {} from database: {e:?}", follow.userid); None } } }) .collect::>() ) } Err(e) => Err(DatabaseError::new( format!("Failed to connect to the database: {e:?}"), "Database connection failure", ) .into()), } } } #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[diesel(table_name = langandagents)] pub struct LangAndAgent { id: i32, agent: String, language: Uuid, relationship: AgentLanguageRelation, } #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[diesel(table_name = langtranslatesto)] pub struct LangTranslatesTo { id: i32, langfrom: Uuid, 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 { pub id: i32, 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", )) } } }