Fragment graphql module, add Appwrite vars to context

This commit is contained in:
Lucien Cartier-Tilet 2023-01-15 17:35:43 +01:00
parent 34e28384ce
commit b20fb5f079
Signed by: phundrak
GPG Key ID: BD7789E705CB8DCA
11 changed files with 280 additions and 146 deletions

View File

@ -26,10 +26,16 @@ uuid = { version = "1.2.2", features = ["v4", "fast-rng", "macro-diagnostics", "
rocket = "0.5.0-rc.2" rocket = "0.5.0-rc.2"
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e814" } rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e814" }
# Web requests
reqwest = { version = "0.11.13", features = ["serde_json", "json", "gzip"] }
# GraphQL # GraphQL
juniper = "0.15.10" juniper = "0.15.10"
juniper_rocket = "0.8.2" juniper_rocket = "0.8.2"
# logging # logging
tracing = "0.1.37" tracing = "0.1.37"
tracing-subscriber = "0.3.16" tracing-subscriber = "0.3.16"
# Error handling
color-eyre = "0.6.2"

88
src/appwrite.rs Normal file
View File

@ -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<bool> {
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::<UserSessions>()
.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<Sessions>,
}
#[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,
}

View File

@ -29,6 +29,7 @@ macro_rules! find_element {
use diesel::prelude::*; use diesel::prelude::*;
#[derive(Debug)]
pub struct Database { pub struct Database {
conn: Pool<ConnectionManager<PgConnection>>, conn: Pool<ConnectionManager<PgConnection>>,
} }

View File

@ -1,4 +1,4 @@
use crate::db::Database; use crate::{db::Database, graphql::Context};
use diesel::prelude::*; use diesel::prelude::*;
use juniper::GraphQLEnum; use juniper::GraphQLEnum;
use tracing::info; use tracing::info;
@ -63,11 +63,11 @@ pub struct Language {
impl Language { impl Language {
fn relationship( fn relationship(
&self, &self,
context: &Database, db: &Database,
relationship: AgentLanguageRelation, relationship: AgentLanguageRelation,
) -> Vec<User> { ) -> Vec<User> {
use schema::langandagents::dsl; use schema::langandagents::dsl;
match &mut context.conn() { match &mut db.conn() {
Ok(conn) => dsl::langandagents Ok(conn) => dsl::langandagents
.filter(dsl::language.eq(self.id)) .filter(dsl::language.eq(self.id))
.filter(dsl::relationship.eq(relationship)) .filter(dsl::relationship.eq(relationship))
@ -97,7 +97,7 @@ impl Language {
} }
} }
#[juniper::graphql_object(Context = Database)] #[juniper::graphql_object(Context = Context)]
impl Language { impl Language {
#[graphql(description = "Unique identifier of the language")] #[graphql(description = "Unique identifier of the language")]
fn id(&self) -> String { fn id(&self) -> String {
@ -125,9 +125,9 @@ impl Language {
name = "targetLanguage", name = "targetLanguage",
description = "Languages in which the current language is translated" description = "Languages in which the current language is translated"
)] )]
fn target_language(&self, context: &Database) -> Vec<Language> { fn target_language(&self, context: &Context) -> Vec<Language> {
use schema::langtranslatesto::dsl; use schema::langtranslatesto::dsl;
match &mut context.conn() { match &mut context.db.conn() {
Ok(conn) => dsl::langtranslatesto Ok(conn) => dsl::langtranslatesto
.filter(dsl::langfrom.eq(self.id)) .filter(dsl::langfrom.eq(self.id))
.load::<LangTranslatesTo>(conn) .load::<LangTranslatesTo>(conn)
@ -187,9 +187,9 @@ impl Language {
#[graphql( #[graphql(
description = "User with administrative rights over the language" description = "User with administrative rights over the language"
)] )]
fn owner(&self, context: &Database) -> User { fn owner(&self, context: &Context) -> User {
use schema::users::dsl; use schema::users::dsl;
match &mut context.conn() { match &mut context.db.conn() {
Ok(conn) => dsl::users Ok(conn) => dsl::users
.find(self.owner.clone()) .find(self.owner.clone())
.first::<User>(conn) .first::<User>(conn)
@ -206,15 +206,15 @@ impl Language {
#[graphql( #[graphql(
description = "People who participate in the elaboration of the language's dictionary" description = "People who participate in the elaboration of the language's dictionary"
)] )]
fn authors(&self, context: &Database) -> Vec<User> { fn authors(&self, context: &Context) -> Vec<User> {
self.relationship(context, AgentLanguageRelation::Author) self.relationship(&context.db, AgentLanguageRelation::Author)
} }
#[graphql( #[graphql(
description = "People who can and do redistribute the language's dictionary" description = "People who can and do redistribute the language's dictionary"
)] )]
fn publishers(&self, context: &Database) -> Vec<User> { fn publishers(&self, context: &Context) -> Vec<User> {
self.relationship(context, AgentLanguageRelation::Publisher) self.relationship(&context.db, AgentLanguageRelation::Publisher)
} }
} }

View File

@ -1,15 +1,15 @@
use super::super::schema::{userfollows, users}; use super::super::schema::{userfollows, users};
use diesel::prelude::*; use diesel::prelude::*;
use crate::db::Database; use crate::graphql::Context;
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
pub struct User { pub struct User {
pub id: String, id: String,
pub username: String, username: String,
} }
#[juniper::graphql_object(Context = Database)] #[juniper::graphql_object(Context = Context)]
impl User { impl User {
#[graphql(description = "Appwrite ID of the user")] #[graphql(description = "Appwrite ID of the user")]
pub fn id(&self) -> String { pub fn id(&self) -> String {
@ -22,9 +22,9 @@ impl User {
} }
#[graphql(description = "Who the user follows")] #[graphql(description = "Who the user follows")]
pub fn following(&self, context: &Database) -> Vec<User> { pub fn following(&self, context: &Context) -> Vec<User> {
use super::super::schema::{userfollows, users}; use super::super::schema::{userfollows, users};
let conn = &mut context.conn().unwrap(); let conn = &mut context.db.conn().unwrap();
userfollows::dsl::userfollows userfollows::dsl::userfollows
.filter(userfollows::dsl::follower.eq(self.id.clone())) .filter(userfollows::dsl::follower.eq(self.id.clone()))
.load::<UserFollow>(conn) .load::<UserFollow>(conn)

View File

@ -1,5 +1,5 @@
use super::super::schema; use super::super::schema;
use crate::db::Database; use crate::{db::Database, graphql::Context};
use diesel::prelude::*; use diesel::prelude::*;
use juniper::GraphQLEnum; use juniper::GraphQLEnum;
use schema::{wordrelation, words}; use schema::{wordrelation, words};
@ -60,11 +60,11 @@ pub struct Word {
impl Word { impl Word {
fn relationship( fn relationship(
&self, &self,
context: &Database, db: &Database,
relationship: WordRelationship, relationship: WordRelationship,
) -> Vec<Word> { ) -> Vec<Word> {
use schema::wordrelation::dsl; use schema::wordrelation::dsl;
match &mut context.conn() { match &mut db.conn() {
Ok(conn) => dsl::wordrelation Ok(conn) => dsl::wordrelation
.filter(dsl::wordsource.eq(self.norm.clone())) .filter(dsl::wordsource.eq(self.norm.clone()))
.filter(dsl::relationship.eq(relationship)) .filter(dsl::relationship.eq(relationship))
@ -84,7 +84,7 @@ impl Word {
} }
} }
#[juniper::graphql_object(Context = Database)] #[juniper::graphql_object(Context = Context)]
impl Word { impl Word {
#[graphql(description = "Normal form of the word")] #[graphql(description = "Normal form of the word")]
fn norm(&self) -> String { fn norm(&self) -> String {
@ -97,10 +97,10 @@ impl Word {
} }
#[graphql(description = "Base form of the current word")] #[graphql(description = "Base form of the current word")]
fn lemma(&self, context: &Database) -> Option<Word> { fn lemma(&self, context: &Context) -> Option<Word> {
use schema::words::dsl; use schema::words::dsl;
match self.lemma.clone() { match self.lemma.clone() {
Some(lemma) => match &mut context.conn() { Some(lemma) => match &mut context.db.conn() {
Ok(conn) => { Ok(conn) => {
match dsl::words.find(lemma.clone()).first::<Word>(conn) { match dsl::words.find(lemma.clone()).first::<Word>(conn) {
Ok(word) => Some(word), Ok(word) => Some(word),
@ -123,9 +123,9 @@ impl Word {
} }
#[graphql(description = "Language to which the word belongs")] #[graphql(description = "Language to which the word belongs")]
fn language(&self, context: &Database) -> Language { fn language(&self, context: &Context) -> Language {
use schema::languages::dsl; use schema::languages::dsl;
match &mut context.conn() { match &mut context.db.conn() {
Ok(conn) => { Ok(conn) => {
match dsl::languages.find(self.language).first::<Language>(conn) match dsl::languages.find(self.language).first::<Language>(conn)
{ {
@ -185,16 +185,16 @@ impl Word {
name = "related", name = "related",
description = "Words related to the current word" description = "Words related to the current word"
)] )]
fn related_words(&self, context: &Database) -> Vec<Word> { fn related_words(&self, context: &Context) -> Vec<Word> {
self.relationship(context, WordRelationship::Related) self.relationship(&context.db, WordRelationship::Related)
} }
#[graphql( #[graphql(
name = "definitions", name = "definitions",
description = "Words that define the current word" description = "Words that define the current word"
)] )]
fn definitions(&self, context: &Database) -> Vec<Word> { fn definitions(&self, context: &Context) -> Vec<Word> {
self.relationship(context, WordRelationship::Definition) self.relationship(&context.db, WordRelationship::Definition)
} }
} }

View File

@ -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<Language> {
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<Language> {
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<User> {
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<Word> {
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<Word> {
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<Database>>;
pub fn create_schema() -> Schema {
Schema::new(Query {}, Mutation {}, EmptySubscription::default())
}
#[rocket::get("/")]
pub fn graphiql() -> RawHtml<String> {
let graphql_endpoint_url = "/graphql";
juniper_rocket::graphiql_source(graphql_endpoint_url, None)
}
#[rocket::get("/graphql?<request>")]
pub async fn get_graphql_handler(
context: &State<Database>,
request: GraphQLRequest,
schema: &State<Schema>,
) -> GraphQLResponse {
request.execute(schema, context).await
}
#[allow(clippy::needless_pass_by_value)]
#[rocket::post("/graphql", data = "<request>")]
pub fn post_graphql_handler(
context: &State<Database>,
request: GraphQLRequest,
schema: &State<Schema>,
) -> GraphQLResponse {
request.execute_sync(schema, context)
}

54
src/graphql/mod.rs Normal file
View File

@ -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<Context>>;
pub fn create_schema() -> Schema {
Schema::new(Query {}, Mutation {}, EmptySubscription::default())
}
#[rocket::get("/")]
pub fn graphiql() -> RawHtml<String> {
let graphql_endpoint_url = "/graphql";
juniper_rocket::graphiql_source(graphql_endpoint_url, None)
}
#[rocket::get("/graphql?<request>")]
pub async fn get_graphql_handler(
context: &State<Context>,
request: GraphQLRequest,
schema: &State<Schema>,
) -> GraphQLResponse {
request.execute(schema, context).await
}
#[allow(clippy::needless_pass_by_value)]
#[rocket::post("/graphql", data = "<request>")]
pub async fn post_graphql_handler(
context: &State<Context>,
request: GraphQLRequest,
schema: &State<Schema>,
) -> GraphQLResponse {
request.execute(schema, context).await
}

32
src/graphql/mutation.rs Normal file
View File

@ -0,0 +1,32 @@
use super::Context;
pub struct Mutation;
#[juniper::graphql_object(Context = Context)]
impl Mutation {
fn api_version(
session_id: Option<String>,
user_id: Option<String>,
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)"
// }
}
}

65
src/graphql/query.rs Normal file
View File

@ -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<Language> {
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<Language> {
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<User> {
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<Word> {
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<Word> {
context
.db
.words(uuid::Uuid::from_str(&language).unwrap(), word.as_str())
}
}

View File

@ -1,5 +1,6 @@
#![warn(clippy::style, clippy::pedantic)] #![warn(clippy::style, clippy::pedantic)]
mod appwrite;
mod db; mod db;
mod graphql; mod graphql;
@ -49,6 +50,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
create_schema, get_graphql_handler, graphiql, post_graphql_handler, create_schema, get_graphql_handler, graphiql, post_graphql_handler,
}; };
color_eyre::install()?;
setup_logging(); setup_logging();
info!("Reading environment variables"); info!("Reading environment variables");
@ -60,7 +62,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::default()) .manage(graphql::Context::default())
.manage(create_schema()) .manage(create_schema())
.mount( .mount(
"/", "/",