From ecd8f58542b16698e5e2e3e7aee735abdc7ae51f Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Wed, 4 Jan 2023 22:16:48 +0100 Subject: [PATCH] Plenty of GraphQL query implementation Implement query for languages, words, initial implementation for user query --- src/db/mod.rs | 106 ++++++++++++++++++---- src/db/models/languages.rs | 177 ++++++++++++++++++++++++++++++++++--- src/db/models/users.rs | 34 +++++-- src/db/models/words.rs | 143 +++++++++++++++++++++++++++++- src/graphql.rs | 35 ++++++-- src/main.rs | 2 +- 6 files changed, 454 insertions(+), 43 deletions(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index 010dd33..267d670 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -2,14 +2,32 @@ 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 dotenvy::dotenv; use std::env; use tracing::info; +macro_rules! find_element { + ($conn:expr,$dsl:ident,$type:ty,$value:expr,$errmsg:expr) => { + if let Ok(val) = $conn { + $dsl.find($value).first::<$type>(val).map_or_else( + |e| { + info!("{}: {:?}", $errmsg, e); + None + }, + Some, + ) + } else { + info!("Failed to obtain connection for the database"); + None + } + }; +} + use diesel::prelude::*; -// use diesel::query_dsl::RunQueryDsl; pub struct Database { conn: Pool>, @@ -17,13 +35,15 @@ pub struct Database { impl juniper::Context for Database {} -impl Database { - pub fn new() -> Self { +impl Default for Database { + fn default() -> Self { Self { conn: Database::get_connection_pool(), } } +} +impl Database { pub fn get_connection_pool() -> Pool> { dotenv().ok(); let database_url = @@ -51,23 +71,73 @@ impl Database { }) } - pub fn language(&self, name: &str) -> Option { - use self::schema::languages::dsl::languages; + pub fn language(&self, name: &str, owner: &str) -> Option { + use self::schema::languages::dsl; match &mut self.conn() { - Ok(val) => languages - .find(name.to_string()) - .first::(val) - .map_or_else( - |e| { - info!( - "Failed to retrieve language {} from database: {:?}", - name, e + Ok(conn) => match dsl::languages + .filter(dsl::name.eq(name)) + .filter(dsl::owner.eq(owner)) + .first::(conn) + { + Ok(val) => Some(val), + Err(e) => { + info!("Could not retrieve language {} of user {} from database: {:?}", + name, owner, e); + None + } + }, + Err(e) => { + info!("Could not connect to the database: {:?}", e); + None + } + } + } + + pub fn user(&self, id: &str) -> Option { + use self::schema::users::dsl::users; + find_element!( + &mut self.conn(), + users, + User, + id.to_string(), + format!("Failed to retrieve user {} from database", id) + ) + } + + pub fn word_id(&self, id: &str) -> Option { + use self::schema::words::dsl; + if let Ok(conn) = &mut self.conn() { + match dsl::words.find(id).first::(conn) { + Ok(val) => Some(val), + Err(e) => { + info!("Error retrieving {}: {:?}", id, e); + None + } + } + } else { + None + } + } + + pub fn words(&self, language: &str, word: &str) -> Vec { + use self::schema::words::dsl; + if let Ok(conn) = &mut self.conn() { + match dsl::words + .filter(dsl::language.eq(language)) + .filter(dsl::norm.eq(word)) + .load::(conn) + { + Ok(val) => val, + Err(e) => { + info!( + "Error retrieving {} from language {}: {:?}", + word, language, e ); - None - }, - Some, - ), - Err(_) => None, + Vec::new() + } + } + } else { + Vec::new() } } } diff --git a/src/db/models/languages.rs b/src/db/models/languages.rs index 3b7cb4d..2922b87 100644 --- a/src/db/models/languages.rs +++ b/src/db/models/languages.rs @@ -1,18 +1,28 @@ -use super::super::schema::{langandagents, languages}; +use crate::db::Database; use diesel::prelude::*; use juniper::GraphQLEnum; +use tracing::info; -#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum)] +use super::super::schema; +use super::users::User; + +use schema::{langandagents, languages}; + +#[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")] + #[graphql(name = "NON_COMMERCIAL")] NonCommercial, Research, Private, } -#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum)] +#[derive( + diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, +)] #[DieselTypePath = "crate::db::schema::sql_types::Dictgenre"] pub enum DictGenre { General, @@ -24,7 +34,9 @@ pub enum DictGenre { Terminology, } -#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum)] +#[derive( + diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, +)] #[DieselTypePath = "crate::db::schema::sql_types::Agentlanguagerelation"] pub enum AgentLanguageRelation { Publisher, @@ -46,21 +58,163 @@ pub struct Language { owner: String, } -#[juniper::graphql_object] impl Language { - #[graphql(name = "release")] + fn relationship( + &self, + context: &Database, + relationship: AgentLanguageRelation, + ) -> Vec { + use schema::langandagents::dsl; + match &mut context.conn() { + Ok(conn) => dsl::langandagents + .filter(dsl::language.eq(self.name.clone())) + .filter(dsl::relationship.eq(relationship)) + .load::(conn) + .unwrap() + .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::>(), + Err(e) => { + panic!("Could not connect to the database: {:?}", e); + } + } + } +} + +#[juniper::graphql_object(Context = Database)] +impl Language { + #[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() -> Option { + self.native.clone() + } + + #[graphql(description = "How the dictionary is currently released")] fn release(&self) -> Release { self.release.clone() } - #[graphql(name = "created")] + #[graphql( + name = "targetLanguage", + description = "Languages in which the current language is translated" + )] + fn target_language(&self, context: &Database) -> Vec { + use schema::languages::dsl; + match &mut context.conn() { + Ok(conn) => self + .targetlanguage + .clone() + .into_iter() + .flatten() + .map(|l| dsl::languages.find(l).first::(conn)) + .filter_map(|l| match l { + Ok(language) => Some(language), + Err(e) => { + info!( + "Failed to retrieve language from database: {:?}", + e + ); + None + } + }) + .collect::>(), + Err(e) => { + info!("Failed to connect to the database: {:?}", e); + Vec::new() + } + } + } + + #[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(name = "name")] - fn name(&self) -> String { - self.name.clone() + #[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: &Database) -> User { + use schema::users::dsl; + match &mut context.conn() { + Ok(conn) => dsl::users + .find(self.owner.clone()) + .first::(conn) + .unwrap_or_else(|e| { + panic!( + "Failed to retrieve owner {} of language {}: {:?}", + self.owner, self.name, e + ) + }), + Err(e) => panic!("Failed to connect to the database: {:?}", e), + } + } + + #[graphql( + description = "People who participate in the elaboration of the language's dictionary" + )] + fn authors(&self, context: &Database) -> Vec { + self.relationship(context, AgentLanguageRelation::Author) + } + + #[graphql( + description = "People who can and do redistribute the language's dictionary" + )] + fn publishers(&self, context: &Database) -> Vec { + self.relationship(context, AgentLanguageRelation::Publisher) } } @@ -70,4 +224,5 @@ pub struct LangAndAgent { id: i32, agent: String, language: String, + relationship: AgentLanguageRelation, } diff --git a/src/db/models/users.rs b/src/db/models/users.rs index d52b0a5..ddc98ce 100644 --- a/src/db/models/users.rs +++ b/src/db/models/users.rs @@ -1,5 +1,7 @@ -use diesel::prelude::*; use super::super::schema::{userfollows, users}; +use diesel::prelude::*; + +use crate::db::Database; #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] pub struct User { @@ -7,14 +9,34 @@ pub struct User { pub username: String, } -#[juniper::graphql_object] +#[juniper::graphql_object(Context = Database)] impl User { - pub fn id(&self) -> &str { - self.id.as_str() + #[graphql(description = "Appwrite ID of the user")] + pub fn id(&self) -> String { + self.id.clone() } - pub fn username(&self) -> &str { - self.username.as_str() + #[graphql(description = "The user's apparent name")] + pub fn username(&self) -> String { + self.username.clone() + } + + #[graphql(description = "Who the user follows")] + pub fn following(&self, context: &Database) -> Vec { + use super::super::schema::{userfollows, users}; + let conn = &mut context.conn().unwrap(); + userfollows::dsl::userfollows + .filter(userfollows::dsl::follower.eq(self.id.clone())) + .load::(conn) + .unwrap() + .iter() + .map(|f| { + users::dsl::users + .find(f.following.clone()) + .first::(conn) + .unwrap() + }) + .collect::>() } } diff --git a/src/db/models/words.rs b/src/db/models/words.rs index ecba2d0..a7d0bf5 100644 --- a/src/db/models/words.rs +++ b/src/db/models/words.rs @@ -1,5 +1,11 @@ -use super::super::schema::{wordrelation, words}; +use super::super::schema; +use crate::db::Database; use diesel::prelude::*; +use juniper::GraphQLEnum; +use schema::{wordrelation, words}; +use tracing::info; + +use super::languages::Language; #[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq)] #[DieselTypePath = "crate::db::schema::sql_types::Wordrelationship"] @@ -8,13 +14,14 @@ pub enum WordRelationship { Related, } -#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq)] +#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum)] #[DieselTypePath = "crate::db::schema::sql_types::Partofspeech"] pub enum PartOfSpeech { Adjective, Adposition, Adverb, Auxilliary, + #[graphql(name = "COORDINATING_CONJUNCTION")] CoordConj, Determiner, Interjection, @@ -22,8 +29,10 @@ pub enum PartOfSpeech { Numeral, Particle, Pronoun, + #[graphql(name = "PROPER_NOUN")] ProperNoun, Punctuation, + #[graphql(name = "SUBORDINATING_CONJUNCTION")] SubjConj, Symbol, Verb, @@ -46,6 +55,136 @@ pub struct Word { morphology: Option, } +impl Word { + fn relationship(&self, context: &Database, relationship: WordRelationship) -> Vec { + use schema::wordrelation::dsl; + match &mut context.conn() { + Ok(conn) => { + dsl::wordrelation + .filter(dsl::wordsource.eq(self.norm.clone())) + .filter(dsl::relationship.eq(relationship)) + .load::(conn) + .unwrap() + .into_iter() + .flat_map(|w| { + use schema::words::dsl; + dsl::words.find(w.wordtarget).first::(conn) + }) + .collect::>() + }, + Err(e) => { + info!("Could not connect to database: {:?}", e); + Vec::new() + } + } + } + +} + +#[juniper::graphql_object(Context = Database)] +impl Word { + #[graphql(description = "Normal form of the word")] + fn norm(&self) -> String { + self.norm.clone() + } + + #[graphql(description = "Native representation of the word")] + fn native(&self) -> Option { + self.native.clone() + } + + #[graphql(description = "Base form of the current word")] + fn lemma(&self, context: &Database) -> Option { + use schema::words::dsl; + match self.lemma.clone() { + Some(lemma) => match &mut context.conn() { + Ok(conn) => match dsl::words.find(lemma.clone()).first::(conn) { + Ok(word) => Some(word), + Err(e) => { + info!( + "Failed to retrieve lemma {} of word {}: {:?}", + lemma, self.norm, e + ); + None + } + }, + Err(e) => { + info!("Could not connect to the database: {:?}", e); + None + } + }, + None => None, + } + } + + #[graphql(description = "Language to which the word belongs")] + fn language(&self, context: &Database) -> Language { + use schema::languages::dsl; + match &mut context.conn() { + Ok(conn) => { + match dsl::languages + .find(self.language.clone()) + .first::(conn) + { + Ok(lang) => lang, + Err(e) => { + panic!("Failed to retrieve language {} of word {} from database: {:?}", + self.language, self.norm, e + ) + } + } + } + Err(e) => panic!("Failed to connect to database: {:?}", e), + } + } + + #[graphql(name = "partOfSpeech", description = "Part of speech the word belongs to")] + fn part_of_speech(&self) -> PartOfSpeech { + self.partofspeech.clone() + } + + #[graphql(description = "Link to an audio file related to the word")] + fn audio(&self) -> Option { + self.audio.clone() + } + + #[graphql(description = "Link to an video file related to the word")] + fn video(&self) -> Option { + self.video.clone() + } + + #[graphql(description = "Link to an image file related to the word")] + fn image(&self) -> Option { + self.image.clone() + } + + #[graphql(description = "Etymology of the word, can be in Markdown format")] + fn etymology(&self) -> Option { + self.etymology.clone() + } + + #[graphql(description = "Usage of the word, can be in Markdown format")] + fn usage(&self) -> Option { + self.lusage.clone() + } + + + #[graphql(description = "Morphology of the word, can be in Markdown format")] + fn morphology(&self) -> Option { + self.morphology.clone() + } + + #[graphql(name = "related", description = "Words related to the current word")] + fn related_words(&self, context: &Database) -> Vec { + self.relationship(context, WordRelationship::Related) + } + + #[graphql(name = "definitions", description = "Words that define the current word")] + fn definitions(&self, context: &Database) -> Vec { + self.relationship(context, WordRelationship::Definition) + } +} + #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[diesel(table_name = wordrelation)] pub struct WordRelation { diff --git a/src/graphql.rs b/src/graphql.rs index 77f9e1d..4c62f86 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -4,7 +4,7 @@ use rocket::State; use juniper::EmptySubscription; use juniper_rocket::{GraphQLRequest, GraphQLResponse}; -use crate::db::models::languages::Language; +use crate::db::models::{languages::Language, users::User, words::Word}; use crate::db::Database; #[derive(Debug)] @@ -17,8 +17,32 @@ impl Query { context.all_languages().unwrap() } - fn language(context: &Database, name: String) -> Option { - context.language(name.as_str()) + #[graphql( + description = "Retrieve a specific language from its name and its owner's id" + )] + fn language( + context: &Database, + name: String, + owner: String, + ) -> Option { + context.language(name.as_str(), owner.as_str()) + } + + #[graphql(description = "Retrieve a specific user from its id")] + fn user(context: &Database, id: String) -> Option { + context.user(id.as_str()) + } + + #[graphql(description = "Retrieve a specific word from its id")] + fn word(context: &Database, id: String) -> Option { + context.word_id(id.as_str()) + } + + #[graphql( + description = "Retrieve all words with a set normal form from a set language" + )] + fn words(context: &Database, language: String, word: String) -> Vec { + context.words(language.as_str(), word.as_str()) } } @@ -31,7 +55,8 @@ impl Mutation { } } -type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription>; +type Schema = + juniper::RootNode<'static, Query, Mutation, EmptySubscription>; pub fn create_schema() -> Schema { Schema::new(Query {}, Mutation {}, EmptySubscription::default()) @@ -57,7 +82,7 @@ pub async fn get_graphql_handler( pub fn post_graphql_handler( context: &State, request: GraphQLRequest, - schema: &State + schema: &State, ) -> GraphQLResponse { request.execute_sync(schema, context) } diff --git a/src/main.rs b/src/main.rs index b4e774e..1b35746 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,7 +60,7 @@ async fn main() -> Result<(), Box> { #[allow(clippy::let_underscore_drop, clippy::no_effect_underscore_binding)] let _ = rocket::build() .attach(cors) - .manage(db::Database::new()) + .manage(db::Database::default()) .manage(create_schema()) .mount( "/",