Plenty of GraphQL query implementation

Implement query for languages, words, initial implementation for user
query
This commit is contained in:
Lucien Cartier-Tilet 2023-01-04 22:16:48 +01:00
parent c0b0a53061
commit ecd8f58542
Signed by: phundrak
GPG Key ID: BD7789E705CB8DCA
6 changed files with 454 additions and 43 deletions

View File

@ -2,14 +2,32 @@ pub mod models;
pub mod schema; pub mod schema;
use self::models::languages::Language; use self::models::languages::Language;
use self::models::users::User;
use self::models::words::Word;
use diesel::pg::PgConnection; use diesel::pg::PgConnection;
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
use dotenvy::dotenv; use dotenvy::dotenv;
use std::env; use std::env;
use tracing::info; 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::prelude::*;
// use diesel::query_dsl::RunQueryDsl;
pub struct Database { pub struct Database {
conn: Pool<ConnectionManager<PgConnection>>, conn: Pool<ConnectionManager<PgConnection>>,
@ -17,13 +35,15 @@ pub struct Database {
impl juniper::Context for Database {} impl juniper::Context for Database {}
impl Database { impl Default for Database {
pub fn new() -> Self { fn default() -> Self {
Self { Self {
conn: Database::get_connection_pool(), conn: Database::get_connection_pool(),
} }
} }
}
impl Database {
pub fn get_connection_pool() -> Pool<ConnectionManager<PgConnection>> { pub fn get_connection_pool() -> Pool<ConnectionManager<PgConnection>> {
dotenv().ok(); dotenv().ok();
let database_url = let database_url =
@ -51,23 +71,73 @@ impl Database {
}) })
} }
pub fn language(&self, name: &str) -> Option<Language> { pub fn language(&self, name: &str, owner: &str) -> Option<Language> {
use self::schema::languages::dsl::languages; use self::schema::languages::dsl;
match &mut self.conn() { match &mut self.conn() {
Ok(val) => languages Ok(conn) => match dsl::languages
.find(name.to_string()) .filter(dsl::name.eq(name))
.first::<Language>(val) .filter(dsl::owner.eq(owner))
.map_or_else( .first::<Language>(conn)
|e| { {
info!( Ok(val) => Some(val),
"Failed to retrieve language {} from database: {:?}", Err(e) => {
name, 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<User> {
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<Word> {
use self::schema::words::dsl;
if let Ok(conn) = &mut self.conn() {
match dsl::words.find(id).first::<Word>(conn) {
Ok(val) => Some(val),
Err(e) => {
info!("Error retrieving {}: {:?}", id, e);
None
}
}
} else {
None
}
}
pub fn words(&self, language: &str, word: &str) -> Vec<Word> {
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::<Word>(conn)
{
Ok(val) => val,
Err(e) => {
info!(
"Error retrieving {} from language {}: {:?}",
word, language, e
); );
None Vec::new()
}, }
Some, }
), } else {
Err(_) => None, Vec::new()
} }
} }
} }

View File

@ -1,18 +1,28 @@
use super::super::schema::{langandagents, languages}; use crate::db::Database;
use diesel::prelude::*; use diesel::prelude::*;
use juniper::GraphQLEnum; 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"] #[DieselTypePath = "crate::db::schema::sql_types::Release"]
pub enum Release { pub enum Release {
Public, Public,
#[graphql(name="NON_COMMERCIAL")] #[graphql(name = "NON_COMMERCIAL")]
NonCommercial, NonCommercial,
Research, Research,
Private, 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"] #[DieselTypePath = "crate::db::schema::sql_types::Dictgenre"]
pub enum DictGenre { pub enum DictGenre {
General, General,
@ -24,7 +34,9 @@ pub enum DictGenre {
Terminology, 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"] #[DieselTypePath = "crate::db::schema::sql_types::Agentlanguagerelation"]
pub enum AgentLanguageRelation { pub enum AgentLanguageRelation {
Publisher, Publisher,
@ -46,21 +58,163 @@ pub struct Language {
owner: String, owner: String,
} }
#[juniper::graphql_object]
impl Language { impl Language {
#[graphql(name = "release")] fn relationship(
&self,
context: &Database,
relationship: AgentLanguageRelation,
) -> Vec<User> {
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::<LangAndAgent>(conn)
.unwrap()
.iter()
.map(|v| {
use schema::users::dsl;
dsl::users.find(v.agent.clone()).first::<User>(conn)
})
.filter_map(|author| match author {
Ok(val) => Some(val),
Err(e) => {
info!(
"Failed ot retrieve author from database: {:?}",
e
);
None
}
})
.collect::<Vec<User>>(),
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<String> {
self.native.clone()
}
#[graphql(description = "How the dictionary is currently released")]
fn release(&self) -> Release { fn release(&self) -> Release {
self.release.clone() 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<Language> {
use schema::languages::dsl;
match &mut context.conn() {
Ok(conn) => self
.targetlanguage
.clone()
.into_iter()
.flatten()
.map(|l| dsl::languages.find(l).first::<Language>(conn))
.filter_map(|l| match l {
Ok(language) => Some(language),
Err(e) => {
info!(
"Failed to retrieve language from database: {:?}",
e
);
None
}
})
.collect::<Vec<Language>>(),
Err(e) => {
info!("Failed to connect to the database: {:?}", e);
Vec::new()
}
}
}
#[graphql(description = "What kind of dictionary this is")]
fn genre(&self) -> Vec<DictGenre> {
self.genre.clone().into_iter().flatten().collect()
}
#[graphql(
name = "abstract",
description = "Short description of the language"
)]
fn abstract_(&self) -> Option<String> {
self.abstract_.clone()
}
#[graphql(
description = "Time at which the language's dictionary was created"
)]
fn created(&self) -> String { fn created(&self) -> String {
self.created.to_string() self.created.to_string()
} }
#[graphql(name = "name")] #[graphql(
fn name(&self) -> String { description = "Longer description of the language, its content can be formatted as Markdown"
self.name.clone() )]
fn description(&self) -> Option<String> {
self.description.clone()
}
#[graphql(
description = "Copyrights held by various people over the language's dictionary and its content"
)]
fn rights(&self) -> Option<String> {
self.rights.clone()
}
#[graphql(description = "License under which the dictionary is released")]
fn license(&self) -> Option<String> {
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::<User>(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<User> {
self.relationship(context, AgentLanguageRelation::Author)
}
#[graphql(
description = "People who can and do redistribute the language's dictionary"
)]
fn publishers(&self, context: &Database) -> Vec<User> {
self.relationship(context, AgentLanguageRelation::Publisher)
} }
} }
@ -70,4 +224,5 @@ pub struct LangAndAgent {
id: i32, id: i32,
agent: String, agent: String,
language: String, language: String,
relationship: AgentLanguageRelation,
} }

View File

@ -1,5 +1,7 @@
use diesel::prelude::*;
use super::super::schema::{userfollows, users}; use super::super::schema::{userfollows, users};
use diesel::prelude::*;
use crate::db::Database;
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
pub struct User { pub struct User {
@ -7,14 +9,34 @@ pub struct User {
pub username: String, pub username: String,
} }
#[juniper::graphql_object] #[juniper::graphql_object(Context = Database)]
impl User { impl User {
pub fn id(&self) -> &str { #[graphql(description = "Appwrite ID of the user")]
self.id.as_str() pub fn id(&self) -> String {
self.id.clone()
} }
pub fn username(&self) -> &str { #[graphql(description = "The user's apparent name")]
self.username.as_str() pub fn username(&self) -> String {
self.username.clone()
}
#[graphql(description = "Who the user follows")]
pub fn following(&self, context: &Database) -> Vec<User> {
use super::super::schema::{userfollows, users};
let conn = &mut context.conn().unwrap();
userfollows::dsl::userfollows
.filter(userfollows::dsl::follower.eq(self.id.clone()))
.load::<UserFollow>(conn)
.unwrap()
.iter()
.map(|f| {
users::dsl::users
.find(f.following.clone())
.first::<User>(conn)
.unwrap()
})
.collect::<Vec<User>>()
} }
} }

View File

@ -1,5 +1,11 @@
use super::super::schema::{wordrelation, words}; use super::super::schema;
use crate::db::Database;
use diesel::prelude::*; 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)] #[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq)]
#[DieselTypePath = "crate::db::schema::sql_types::Wordrelationship"] #[DieselTypePath = "crate::db::schema::sql_types::Wordrelationship"]
@ -8,13 +14,14 @@ pub enum WordRelationship {
Related, 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"] #[DieselTypePath = "crate::db::schema::sql_types::Partofspeech"]
pub enum PartOfSpeech { pub enum PartOfSpeech {
Adjective, Adjective,
Adposition, Adposition,
Adverb, Adverb,
Auxilliary, Auxilliary,
#[graphql(name = "COORDINATING_CONJUNCTION")]
CoordConj, CoordConj,
Determiner, Determiner,
Interjection, Interjection,
@ -22,8 +29,10 @@ pub enum PartOfSpeech {
Numeral, Numeral,
Particle, Particle,
Pronoun, Pronoun,
#[graphql(name = "PROPER_NOUN")]
ProperNoun, ProperNoun,
Punctuation, Punctuation,
#[graphql(name = "SUBORDINATING_CONJUNCTION")]
SubjConj, SubjConj,
Symbol, Symbol,
Verb, Verb,
@ -46,6 +55,136 @@ pub struct Word {
morphology: Option<String>, morphology: Option<String>,
} }
impl Word {
fn relationship(&self, context: &Database, relationship: WordRelationship) -> Vec<Word> {
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::<WordRelation>(conn)
.unwrap()
.into_iter()
.flat_map(|w| {
use schema::words::dsl;
dsl::words.find(w.wordtarget).first::<Word>(conn)
})
.collect::<Vec<Word>>()
},
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<String> {
self.native.clone()
}
#[graphql(description = "Base form of the current word")]
fn lemma(&self, context: &Database) -> Option<Word> {
use schema::words::dsl;
match self.lemma.clone() {
Some(lemma) => match &mut context.conn() {
Ok(conn) => match dsl::words.find(lemma.clone()).first::<Word>(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::<Language>(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<String> {
self.audio.clone()
}
#[graphql(description = "Link to an video file related to the word")]
fn video(&self) -> Option<String> {
self.video.clone()
}
#[graphql(description = "Link to an image file related to the word")]
fn image(&self) -> Option<String> {
self.image.clone()
}
#[graphql(description = "Etymology of the word, can be in Markdown format")]
fn etymology(&self) -> Option<String> {
self.etymology.clone()
}
#[graphql(description = "Usage of the word, can be in Markdown format")]
fn usage(&self) -> Option<String> {
self.lusage.clone()
}
#[graphql(description = "Morphology of the word, can be in Markdown format")]
fn morphology(&self) -> Option<String> {
self.morphology.clone()
}
#[graphql(name = "related", description = "Words related to the current word")]
fn related_words(&self, context: &Database) -> Vec<Word> {
self.relationship(context, WordRelationship::Related)
}
#[graphql(name = "definitions", description = "Words that define the current word")]
fn definitions(&self, context: &Database) -> Vec<Word> {
self.relationship(context, WordRelationship::Definition)
}
}
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
#[diesel(table_name = wordrelation)] #[diesel(table_name = wordrelation)]
pub struct WordRelation { pub struct WordRelation {

View File

@ -4,7 +4,7 @@ use rocket::State;
use juniper::EmptySubscription; use juniper::EmptySubscription;
use juniper_rocket::{GraphQLRequest, GraphQLResponse}; 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; use crate::db::Database;
#[derive(Debug)] #[derive(Debug)]
@ -17,8 +17,32 @@ impl Query {
context.all_languages().unwrap() context.all_languages().unwrap()
} }
fn language(context: &Database, name: String) -> Option<Language> { #[graphql(
context.language(name.as_str()) description = "Retrieve a specific language from its name and its owner's id"
)]
fn language(
context: &Database,
name: String,
owner: String,
) -> Option<Language> {
context.language(name.as_str(), owner.as_str())
}
#[graphql(description = "Retrieve a specific user from its id")]
fn user(context: &Database, id: String) -> Option<User> {
context.user(id.as_str())
}
#[graphql(description = "Retrieve a specific word from its id")]
fn word(context: &Database, id: String) -> Option<Word> {
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<Word> {
context.words(language.as_str(), word.as_str())
} }
} }
@ -31,7 +55,8 @@ impl Mutation {
} }
} }
type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription<Database>>; type Schema =
juniper::RootNode<'static, Query, Mutation, EmptySubscription<Database>>;
pub fn create_schema() -> Schema { pub fn create_schema() -> Schema {
Schema::new(Query {}, Mutation {}, EmptySubscription::default()) Schema::new(Query {}, Mutation {}, EmptySubscription::default())
@ -57,7 +82,7 @@ pub async fn get_graphql_handler(
pub fn post_graphql_handler( pub fn post_graphql_handler(
context: &State<Database>, context: &State<Database>,
request: GraphQLRequest, request: GraphQLRequest,
schema: &State<Schema> schema: &State<Schema>,
) -> GraphQLResponse { ) -> GraphQLResponse {
request.execute_sync(schema, context) request.execute_sync(schema, context)
} }

View File

@ -60,7 +60,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
#[allow(clippy::let_underscore_drop, clippy::no_effect_underscore_binding)] #[allow(clippy::let_underscore_drop, clippy::no_effect_underscore_binding)]
let _ = rocket::build() let _ = rocket::build()
.attach(cors) .attach(cors)
.manage(db::Database::new()) .manage(db::Database::default())
.manage(create_schema()) .manage(create_schema())
.mount( .mount(
"/", "/",