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_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"
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::*;
#[derive(Debug)]
pub struct Database {
conn: Pool<ConnectionManager<PgConnection>>,
}

View File

@ -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<User> {
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<Language> {
fn target_language(&self, context: &Context) -> Vec<Language> {
use schema::langtranslatesto::dsl;
match &mut context.conn() {
match &mut context.db.conn() {
Ok(conn) => dsl::langtranslatesto
.filter(dsl::langfrom.eq(self.id))
.load::<LangTranslatesTo>(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::<User>(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<User> {
self.relationship(context, AgentLanguageRelation::Author)
fn authors(&self, context: &Context) -> Vec<User> {
self.relationship(&context.db, 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)
fn publishers(&self, context: &Context) -> Vec<User> {
self.relationship(&context.db, AgentLanguageRelation::Publisher)
}
}

View File

@ -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<User> {
pub fn following(&self, context: &Context) -> Vec<User> {
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::<UserFollow>(conn)

View File

@ -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<Word> {
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<Word> {
fn lemma(&self, context: &Context) -> Option<Word> {
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::<Word>(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::<Language>(conn)
{
@ -185,16 +185,16 @@ impl Word {
name = "related",
description = "Words related to the current word"
)]
fn related_words(&self, context: &Database) -> Vec<Word> {
self.relationship(context, WordRelationship::Related)
fn related_words(&self, context: &Context) -> Vec<Word> {
self.relationship(&context.db, WordRelationship::Related)
}
#[graphql(
name = "definitions",
description = "Words that define the current word"
)]
fn definitions(&self, context: &Database) -> Vec<Word> {
self.relationship(context, WordRelationship::Definition)
fn definitions(&self, context: &Context) -> Vec<Word> {
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)]
mod appwrite;
mod db;
mod graphql;
@ -49,6 +50,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
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<dyn Error>> {
#[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(
"/",