From b20fb5f079b65f985101b320197c8bfd5144a5e6 Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Sun, 15 Jan 2023 17:35:43 +0100 Subject: [PATCH] Fragment graphql module, add Appwrite vars to context --- Cargo.toml | 8 ++- src/appwrite.rs | 88 ++++++++++++++++++++++++++++ src/db/mod.rs | 1 + src/db/models/languages.rs | 24 ++++---- src/db/models/users.rs | 12 ++-- src/db/models/words.rs | 24 ++++---- src/graphql.rs | 114 ------------------------------------- src/graphql/mod.rs | 54 ++++++++++++++++++ src/graphql/mutation.rs | 32 +++++++++++ src/graphql/query.rs | 65 +++++++++++++++++++++ src/main.rs | 4 +- 11 files changed, 280 insertions(+), 146 deletions(-) create mode 100644 src/appwrite.rs delete mode 100644 src/graphql.rs create mode 100644 src/graphql/mod.rs create mode 100644 src/graphql/mutation.rs create mode 100644 src/graphql/query.rs diff --git a/Cargo.toml b/Cargo.toml index 59d1533..ae06362 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,10 +26,16 @@ uuid = { version = "1.2.2", features = ["v4", "fast-rng", "macro-diagnostics", " rocket = "0.5.0-rc.2" rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e814" } +# Web requests +reqwest = { version = "0.11.13", features = ["serde_json", "json", "gzip"] } + # GraphQL juniper = "0.15.10" juniper_rocket = "0.8.2" # logging tracing = "0.1.37" -tracing-subscriber = "0.3.16" \ No newline at end of file +tracing-subscriber = "0.3.16" + +# Error handling +color-eyre = "0.6.2" diff --git a/src/appwrite.rs b/src/appwrite.rs new file mode 100644 index 0000000..b77b2e4 --- /dev/null +++ b/src/appwrite.rs @@ -0,0 +1,88 @@ +use color_eyre::eyre::Result; +use rocket::serde::Deserialize; + +macro_rules! from_env { + ($varname:expr) => { + std::env::var($varname) + .expect(format!("{} must be set!", $varname).as_str()) + }; +} + +#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq)] +pub struct APVariables { + pub endpoint: String, + pub project: String, + pub api_key: String, +} + +impl APVariables { + pub async fn check_session( + &self, + session_id: String, + user_id: String, + ) -> Result { + let client = reqwest::Client::new(); + let url = format!("{}/users/{}/sessions", self.endpoint, user_id); + let response = client + .get(url) + .header("X-Appwrite-Key", self.api_key.clone()) + .header("X-Appwrite-Project", self.project.clone()) + .header("Content-Type", "application/json") + .send() + .await? + .json::() + .await?; + Ok(response.sessions.iter().any(|s| s.id == session_id)) + } +} + +impl Default for APVariables { + fn default() -> Self { + Self { + endpoint: from_env!("APPWRITE_ENDPOINT"), + project: from_env!("APPWRITE_PROJECT"), + api_key: from_env!("APPWRITE_API_KEY"), + } + } +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(crate = "rocket::serde")] +struct UserSessions { + total: i64, + sessions: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(crate = "rocket::serde")] +struct Sessions { + #[serde(rename = "$id")] + id: String, + #[serde(rename = "$createdAt")] + created_at: String, + user_id: String, + expire: String, + provider: String, + provider_uid: String, + provider_access_token: String, + provider_access_token_expiry: String, + provider_refresh_token: String, + ip: String, + os_code: String, + os_name: String, + os_version: String, + client_type: String, + client_code: String, + client_name: String, + client_version: String, + client_engine: String, + client_engine_version: String, + device_name: String, + device_brand: String, + device_model: String, + country_code: String, + country_name: String, + current: bool, +} diff --git a/src/db/mod.rs b/src/db/mod.rs index 86e1428..b575347 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -29,6 +29,7 @@ macro_rules! find_element { use diesel::prelude::*; +#[derive(Debug)] pub struct Database { conn: Pool>, } diff --git a/src/db/models/languages.rs b/src/db/models/languages.rs index 4cfa95c..defd686 100644 --- a/src/db/models/languages.rs +++ b/src/db/models/languages.rs @@ -1,4 +1,4 @@ -use crate::db::Database; +use crate::{db::Database, graphql::Context}; use diesel::prelude::*; use juniper::GraphQLEnum; use tracing::info; @@ -63,11 +63,11 @@ pub struct Language { impl Language { fn relationship( &self, - context: &Database, + db: &Database, relationship: AgentLanguageRelation, ) -> Vec { use schema::langandagents::dsl; - match &mut context.conn() { + match &mut db.conn() { Ok(conn) => dsl::langandagents .filter(dsl::language.eq(self.id)) .filter(dsl::relationship.eq(relationship)) @@ -97,7 +97,7 @@ impl Language { } } -#[juniper::graphql_object(Context = Database)] +#[juniper::graphql_object(Context = Context)] impl Language { #[graphql(description = "Unique identifier of the language")] fn id(&self) -> String { @@ -125,9 +125,9 @@ impl Language { name = "targetLanguage", description = "Languages in which the current language is translated" )] - fn target_language(&self, context: &Database) -> Vec { + fn target_language(&self, context: &Context) -> Vec { use schema::langtranslatesto::dsl; - match &mut context.conn() { + match &mut context.db.conn() { Ok(conn) => dsl::langtranslatesto .filter(dsl::langfrom.eq(self.id)) .load::(conn) @@ -187,9 +187,9 @@ impl Language { #[graphql( description = "User with administrative rights over the language" )] - fn owner(&self, context: &Database) -> User { + fn owner(&self, context: &Context) -> User { use schema::users::dsl; - match &mut context.conn() { + match &mut context.db.conn() { Ok(conn) => dsl::users .find(self.owner.clone()) .first::(conn) @@ -206,15 +206,15 @@ impl Language { #[graphql( description = "People who participate in the elaboration of the language's dictionary" )] - fn authors(&self, context: &Database) -> Vec { - self.relationship(context, AgentLanguageRelation::Author) + fn authors(&self, context: &Context) -> Vec { + self.relationship(&context.db, 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) + fn publishers(&self, context: &Context) -> Vec { + self.relationship(&context.db, AgentLanguageRelation::Publisher) } } diff --git a/src/db/models/users.rs b/src/db/models/users.rs index ddc98ce..3427064 100644 --- a/src/db/models/users.rs +++ b/src/db/models/users.rs @@ -1,15 +1,15 @@ use super::super::schema::{userfollows, users}; use diesel::prelude::*; -use crate::db::Database; +use crate::graphql::Context; #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] pub struct User { - pub id: String, - pub username: String, + id: String, + username: String, } -#[juniper::graphql_object(Context = Database)] +#[juniper::graphql_object(Context = Context)] impl User { #[graphql(description = "Appwrite ID of the user")] pub fn id(&self) -> String { @@ -22,9 +22,9 @@ impl User { } #[graphql(description = "Who the user follows")] - pub fn following(&self, context: &Database) -> Vec { + pub fn following(&self, context: &Context) -> Vec { use super::super::schema::{userfollows, users}; - let conn = &mut context.conn().unwrap(); + let conn = &mut context.db.conn().unwrap(); userfollows::dsl::userfollows .filter(userfollows::dsl::follower.eq(self.id.clone())) .load::(conn) diff --git a/src/db/models/words.rs b/src/db/models/words.rs index a7b876f..7db77b4 100644 --- a/src/db/models/words.rs +++ b/src/db/models/words.rs @@ -1,5 +1,5 @@ use super::super::schema; -use crate::db::Database; +use crate::{db::Database, graphql::Context}; use diesel::prelude::*; use juniper::GraphQLEnum; use schema::{wordrelation, words}; @@ -60,11 +60,11 @@ pub struct Word { impl Word { fn relationship( &self, - context: &Database, + db: &Database, relationship: WordRelationship, ) -> Vec { use schema::wordrelation::dsl; - match &mut context.conn() { + match &mut db.conn() { Ok(conn) => dsl::wordrelation .filter(dsl::wordsource.eq(self.norm.clone())) .filter(dsl::relationship.eq(relationship)) @@ -84,7 +84,7 @@ impl Word { } } -#[juniper::graphql_object(Context = Database)] +#[juniper::graphql_object(Context = Context)] impl Word { #[graphql(description = "Normal form of the word")] fn norm(&self) -> String { @@ -97,10 +97,10 @@ impl Word { } #[graphql(description = "Base form of the current word")] - fn lemma(&self, context: &Database) -> Option { + fn lemma(&self, context: &Context) -> Option { use schema::words::dsl; match self.lemma.clone() { - Some(lemma) => match &mut context.conn() { + Some(lemma) => match &mut context.db.conn() { Ok(conn) => { match dsl::words.find(lemma.clone()).first::(conn) { Ok(word) => Some(word), @@ -123,9 +123,9 @@ impl Word { } #[graphql(description = "Language to which the word belongs")] - fn language(&self, context: &Database) -> Language { + fn language(&self, context: &Context) -> Language { use schema::languages::dsl; - match &mut context.conn() { + match &mut context.db.conn() { Ok(conn) => { match dsl::languages.find(self.language).first::(conn) { @@ -185,16 +185,16 @@ impl Word { name = "related", description = "Words related to the current word" )] - fn related_words(&self, context: &Database) -> Vec { - self.relationship(context, WordRelationship::Related) + fn related_words(&self, context: &Context) -> Vec { + self.relationship(&context.db, WordRelationship::Related) } #[graphql( name = "definitions", description = "Words that define the current word" )] - fn definitions(&self, context: &Database) -> Vec { - self.relationship(context, WordRelationship::Definition) + fn definitions(&self, context: &Context) -> Vec { + self.relationship(&context.db, WordRelationship::Definition) } } diff --git a/src/graphql.rs b/src/graphql.rs deleted file mode 100644 index 1b2a8a5..0000000 --- a/src/graphql.rs +++ /dev/null @@ -1,114 +0,0 @@ -use rocket::response::content::RawHtml; -use rocket::State; - -use juniper::EmptySubscription; -use juniper_rocket::{GraphQLRequest, GraphQLResponse}; - -use crate::db::models::{languages::Language, users::User, words::Word}; -use crate::db::Database; - -use std::str::FromStr; - -#[derive(Debug)] -pub struct Query; - -#[juniper::graphql_object(Context = Database)] -impl Query { - #[graphql( - name = "allLanguages", - description = "Retrieve all languages defined in the database" - )] - fn all_languages(context: &Database) -> Vec { - context.all_languages().unwrap() - } - - #[graphql( - description = "Retrieve a specific language from its name and its owner's id", - arguments( - name(description = "Name of the language"), - owner(description = "ID of the owner of the language") - ) - )] - 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", - arguments(id(description = "Appwrite ID of a user")) - )] - fn user(context: &Database, id: String) -> Option { - context.user(id.as_str()) - } - - #[graphql( - description = "Retrieve a specific word from its id", - arguments(id(description = "Unique identifier of a word")) - )] - 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", - arguments( - owner( - description = "ID of the owner of the language to search a word in" - ), - language(description = "Name of the language to search a word in"), - word(description = "Word to search") - ) - )] - fn words( - context: &Database, - language: String, - word: String, - ) -> Vec { - context.words(uuid::Uuid::from_str(&language).unwrap(), word.as_str()) - } -} - -pub struct Mutation; - -#[juniper::graphql_object(Context = Database)] -impl Mutation { - fn api_version() -> String { - "0.1".into() - } -} - -type Schema = - juniper::RootNode<'static, Query, Mutation, EmptySubscription>; - -pub fn create_schema() -> Schema { - Schema::new(Query {}, Mutation {}, EmptySubscription::default()) -} - -#[rocket::get("/")] -pub fn graphiql() -> RawHtml { - let graphql_endpoint_url = "/graphql"; - juniper_rocket::graphiql_source(graphql_endpoint_url, None) -} - -#[rocket::get("/graphql?")] -pub async fn get_graphql_handler( - context: &State, - request: GraphQLRequest, - schema: &State, -) -> GraphQLResponse { - request.execute(schema, context).await -} - -#[allow(clippy::needless_pass_by_value)] -#[rocket::post("/graphql", data = "")] -pub fn post_graphql_handler( - context: &State, - request: GraphQLRequest, - schema: &State, -) -> GraphQLResponse { - request.execute_sync(schema, context) -} diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs new file mode 100644 index 0000000..3138045 --- /dev/null +++ b/src/graphql/mod.rs @@ -0,0 +1,54 @@ +use rocket::response::content::RawHtml; +use rocket::State; + +use juniper::EmptySubscription; +use juniper_rocket::{GraphQLRequest, GraphQLResponse}; + +use crate::appwrite::APVariables; +use crate::db::Database; + +#[derive(Default, Debug)] +pub struct Context { + pub db: Database, + pub appwrite: APVariables, +} + +impl juniper::Context for Context {} + +mod query; +use query::Query; + +mod mutation; +use mutation::Mutation; + +type Schema = + juniper::RootNode<'static, Query, Mutation, EmptySubscription>; + +pub fn create_schema() -> Schema { + Schema::new(Query {}, Mutation {}, EmptySubscription::default()) +} + +#[rocket::get("/")] +pub fn graphiql() -> RawHtml { + let graphql_endpoint_url = "/graphql"; + juniper_rocket::graphiql_source(graphql_endpoint_url, None) +} + +#[rocket::get("/graphql?")] +pub async fn get_graphql_handler( + context: &State, + request: GraphQLRequest, + schema: &State, +) -> GraphQLResponse { + request.execute(schema, context).await +} + +#[allow(clippy::needless_pass_by_value)] +#[rocket::post("/graphql", data = "")] +pub async fn post_graphql_handler( + context: &State, + request: GraphQLRequest, + schema: &State, +) -> GraphQLResponse { + request.execute(schema, context).await +} diff --git a/src/graphql/mutation.rs b/src/graphql/mutation.rs new file mode 100644 index 0000000..1c468b4 --- /dev/null +++ b/src/graphql/mutation.rs @@ -0,0 +1,32 @@ +use super::Context; + +pub struct Mutation; + +#[juniper::graphql_object(Context = Context)] +impl Mutation { + fn api_version( + session_id: Option, + user_id: Option, + context: &Context, + ) -> String { + "0.1".into() + // if session_id.is_some() && user_id.is_some() { + // match context + // .appwrite + // .check_session(session_id.unwrap(), user_id.unwrap()) + // { + // Ok(true) => "0.1 (authentified)".into(), + // Ok(false) => "0.1 (not authentified)".into(), + // Err(e) => { + // info!( + // "Error while checking if the user is connected: {:?}", + // e + // ); + // "0.1 (auth failed)" + // } + // } + // } else { + // "0.1 (not authentified)" + // } + } +} diff --git a/src/graphql/query.rs b/src/graphql/query.rs new file mode 100644 index 0000000..859970e --- /dev/null +++ b/src/graphql/query.rs @@ -0,0 +1,65 @@ +use super::Context; +use crate::db::models::{languages::Language, users::User, words::Word}; + +use std::str::FromStr; + +#[derive(Debug)] +pub struct Query; + +#[juniper::graphql_object(Context = Context)] +impl Query { + #[graphql( + name = "allLanguages", + description = "Retrieve all languages defined in the database" + )] + fn all_languages(context: &Context) -> Vec { + context.db.all_languages().unwrap() + } + + #[graphql( + description = "Retrieve a specific language from its name and its owner's id", + arguments( + name(description = "Name of the language"), + owner(description = "ID of the owner of the language") + ) + )] + fn language( + context: &Context, + name: String, + owner: String, + ) -> Option { + context.db.language(name.as_str(), owner.as_str()) + } + + #[graphql( + description = "Retrieve a specific user from its id", + arguments(id(description = "Appwrite ID of a user")) + )] + fn user(context: &Context, id: String) -> Option { + context.db.user(id.as_str()) + } + + #[graphql( + description = "Retrieve a specific word from its id", + arguments(id(description = "Unique identifier of a word")) + )] + fn word(context: &Context, id: String) -> Option { + context.db.word_id(id.as_str()) + } + + #[graphql( + description = "Retrieve all words with a set normal form from a set language", + arguments( + owner( + description = "ID of the owner of the language to search a word in" + ), + language(description = "Name of the language to search a word in"), + word(description = "Word to search") + ) + )] + fn words(context: &Context, language: String, word: String) -> Vec { + context + .db + .words(uuid::Uuid::from_str(&language).unwrap(), word.as_str()) + } +} diff --git a/src/main.rs b/src/main.rs index 1b35746..a680039 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![warn(clippy::style, clippy::pedantic)] +mod appwrite; mod db; mod graphql; @@ -49,6 +50,7 @@ async fn main() -> Result<(), Box> { create_schema, get_graphql_handler, graphiql, post_graphql_handler, }; + color_eyre::install()?; setup_logging(); info!("Reading environment variables"); @@ -60,7 +62,7 @@ async fn main() -> Result<(), Box> { #[allow(clippy::let_underscore_drop, clippy::no_effect_underscore_binding)] let _ = rocket::build() .attach(cors) - .manage(db::Database::default()) + .manage(graphql::Context::default()) .manage(create_schema()) .mount( "/",