Implement more mutations

Implement mutations:
- create language
- delete language
- user follows a language
- user unfollows a language
- new word

Context's user_auth now contains either the user's ID or nothing if
not authentificated.
This commit is contained in:
Lucien Cartier-Tilet 2023-01-24 01:05:18 +01:00
parent 51f0fc3108
commit a624636939
Signed by: phundrak
GPG Key ID: BD7789E705CB8DCA
4 changed files with 423 additions and 16 deletions

View File

@ -27,6 +27,12 @@ pub enum Release {
Private, Private,
} }
impl Default for Release {
fn default() -> Self {
Self::Public
}
}
#[derive( #[derive(
diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum,
)] )]
@ -50,6 +56,91 @@ pub enum AgentLanguageRelation {
Author, Author,
} }
#[derive(Default, Debug, Clone, juniper::GraphQLInputObject)]
pub struct NewLanguage {
name: String,
native: Option<String>,
release: Option<Release>,
genre: Vec<DictGenre>,
abstract_: Option<String>,
description: Option<String>,
rights: Option<String>,
license: Option<String>,
}
#[derive(Insertable, Debug, Clone)]
#[diesel(table_name = languages)]
struct NewLanguageInternal {
name: String,
native: Option<String>,
release: Release,
genre: Vec<DictGenre>,
abstract_: Option<String>,
description: Option<String>,
rights: Option<String>,
license: Option<String>,
owner: String,
}
impl From<NewLanguage> for NewLanguageInternal {
fn from(val: NewLanguage) -> Self {
NewLanguageInternal {
name: val.name,
native: val.native,
release: if let Some(release) = val.release {
release
} else {
Release::default()
},
genre: val.genre,
abstract_: val.abstract_,
description: val.description,
rights: val.rights,
license: val.license,
owner: String::new(),
}
}
}
impl NewLanguage {
pub fn insert_db(
&self,
db: &Database,
owner: &str,
) -> Result<Language, DatabaseError> {
use languages::dsl;
let conn = &mut db.conn()?;
match diesel::insert_into(dsl::languages)
.values(NewLanguageInternal {
owner: owner.to_string(),
..self.clone().into()
})
.execute(conn)
{
Ok(_) => dsl::languages
.filter(dsl::name.eq(self.name.clone()))
.filter(dsl::owner.eq(owner))
.first::<Language>(conn)
.map_err(|e| {
DatabaseError::new(
format!(
"Failed to find language {} by user {owner}: {e:?}",
self.name
),
"Database Error",
)
}),
Err(e) => Err(DatabaseError::new(
format!(
"Failed to insert language {} by user {owner}: {e:?}",
self.name
),
"Database Error",
)),
}
}
}
#[derive(Queryable, Insertable, Debug, Clone)] #[derive(Queryable, Insertable, Debug, Clone)]
pub struct Language { pub struct Language {
id: Uuid, id: Uuid,
@ -66,6 +157,59 @@ pub struct Language {
} }
impl Language { impl Language {
pub fn find(
db: &Database,
language: Uuid,
) -> Result<Language, DatabaseError> {
use languages::dsl;
dsl::languages.find(language).first::<Language>(&mut db.conn()?).map_err(|e| match e {
diesel::NotFound => DatabaseError::new(
format!("Language {language} not found"),
"Not Found"
),
e => DatabaseError::new(
format!("Error fetching language {language} from database: {e:?}"),
"Database Error"
)
})
}
pub fn delete(
context: &Context,
language_id: Uuid,
) -> Result<(), DatabaseError> {
use languages::dsl;
let conn = &mut context.db.conn()?;
match dsl::languages.find(language_id)
.first::<Language>(conn)
{
Ok(language) if context.user_auth == Some(language.owner.clone()) => {
match diesel::delete(dsl::languages.find(language_id))
.execute(conn) {
Ok(_) => Ok(()),
Err(e) => Err(DatabaseError::new(
format!("Failed to delete language {language_id}: {e:?}"),
"Database Error"
))
}
},
Ok(language) => {
Err(DatabaseError::new(
format!(
"User {} not allowed to delete other user's language {language_id}",
language.owner),
"Unauthorized"
))
},
Err(e) => {
Err(DatabaseError::new(
format!("Failed to delete language {language_id}: {e:?}"),
"Database Error"
))
}
}
}
fn relationship( fn relationship(
&self, &self,
db: &Database, db: &Database,
@ -290,6 +434,13 @@ pub struct LangTranslatesTo {
langto: Uuid, langto: Uuid,
} }
#[derive(Insertable)]
#[diesel(table_name = userfollowlanguage)]
pub struct UserFollowLanguageInsert {
pub lang: Uuid,
pub userid: String,
}
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
#[diesel(table_name = userfollowlanguage)] #[diesel(table_name = userfollowlanguage)]
pub struct UserFollowLanguage { pub struct UserFollowLanguage {
@ -297,3 +448,71 @@ pub struct UserFollowLanguage {
pub lang: Uuid, pub lang: Uuid,
pub userid: String, pub userid: String,
} }
impl UserFollowLanguage {
pub fn user_follow_language(
context: &Context,
userid: &str,
lang: Uuid,
) -> Result<Language, DatabaseError> {
let conn = &mut context.db.conn()?;
match languages::dsl::languages.find(lang).first::<Language>(conn) {
Err(diesel::NotFound) => Err(DatabaseError::new(
format!("Cannot follow non-existing language {lang}"),
"Invalid Language",
)),
Err(e) => Err(DatabaseError::new(
format!(
"Could not retrieve language {lang} from database: {e:?}"
),
"Database error",
)),
Ok(language) => {
use userfollowlanguage::dsl;
match diesel::insert_into(dsl::userfollowlanguage)
.values(UserFollowLanguageInsert { lang, userid: userid.to_string() })
.execute(conn) {
Ok(_) => Ok(language),
Err(e) => Err(DatabaseError::new(
format!("Failed to follow language {lang} as user {userid}: {e:?}"),
"Database Error"
))
}
}
}
}
pub fn user_unfollow_language(
context: &Context,
userid: &str,
lang: Uuid,
) -> Result<Language, DatabaseError> {
use userfollowlanguage::dsl;
let conn = &mut context.db.conn()?;
match dsl::userfollowlanguage
.filter(dsl::userid.eq(userid.to_string()))
.filter(dsl::lang.eq(lang))
.first::<UserFollowLanguage>(conn) {
Ok(relationship) => {
match diesel::delete(dsl::userfollowlanguage.find(relationship.id))
.execute(conn) {
Ok(_) => Language::find(&context.db, lang),
Err(e) => Err(DatabaseError::new(
format!("Failed to make user {userid} unfollow language {lang}: {e:?}"),
"Database Error"
))
}
},
Err(diesel::NotFound) => {
Err(DatabaseError::new(
format!("User {userid} does not follow language {lang}"),
"Invalid",
))
}
Err(e) => Err(DatabaseError::new(
format!("Failed to retrieve relationship between user {userid} and language {lang} from database: {e:?}"),
"Database Error",
))
}
}
}

View File

@ -5,11 +5,11 @@ use crate::{
}; };
use diesel::prelude::*; use diesel::prelude::*;
use juniper::{FieldResult, GraphQLEnum}; use juniper::{FieldResult, GraphQLEnum};
use schema::{wordrelation, words, wordlearning}; use schema::{wordlearning, wordrelation, words};
use tracing::info; use tracing::info;
use uuid::Uuid; use uuid::Uuid;
use std::convert::Into; use std::{convert::Into, str::FromStr};
use super::languages::Language; use super::languages::Language;
@ -20,11 +20,18 @@ pub enum WordRelationship {
Related, Related,
} }
#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, juniper::GraphQLEnum)] #[derive(
diesel_derive_enum::DbEnum,
Debug,
Clone,
PartialEq,
Eq,
juniper::GraphQLEnum,
)]
#[DieselTypePath = "crate::db::schema::sql_types::Wordlearningstatus"] #[DieselTypePath = "crate::db::schema::sql_types::Wordlearningstatus"]
pub enum WordLearningStatus { pub enum WordLearningStatus {
Learning, Learning,
Learned Learned,
} }
#[derive( #[derive(
@ -54,6 +61,72 @@ pub enum PartOfSpeech {
Other, Other,
} }
impl Default for PartOfSpeech {
fn default() -> Self {
Self::Noun
}
}
#[derive(Debug, Clone, juniper::GraphQLInputObject)]
pub struct NewWord {
norm: String,
native: Option<String>,
lemma: Option<String>,
language: String,
partofspeech: PartOfSpeech,
audio: Option<String>,
video: Option<String>,
image: Option<String>,
description: Option<String>,
etymology: Option<String>,
lusage: Option<String>,
morphology: Option<String>,
}
#[derive(Debug, Clone, Insertable)]
#[diesel(table_name = words)]
struct NewWordInternal {
norm: String,
native: Option<String>,
lemma: Option<Uuid>,
language: Uuid,
partofspeech: PartOfSpeech,
audio: Option<String>,
video: Option<String>,
image: Option<String>,
description: Option<String>,
etymology: Option<String>,
lusage: Option<String>,
morphology: Option<String>,
}
impl TryFrom<NewWord> for NewWordInternal {
type Error = uuid::Error;
fn try_from(value: NewWord) -> Result<Self, Self::Error> {
let language = Uuid::from_str(&value.language)?;
let lemma = if let Some(original_lemma) = value.lemma {
Some(Uuid::from_str(&original_lemma)?)
} else {
None
};
Ok(Self {
norm: value.norm,
native: value.native,
lemma,
language,
partofspeech: value.partofspeech,
audio: value.audio,
video: value.video,
image: value.image,
description: value.description,
etymology: value.etymology,
lusage: value.lusage,
morphology: value.morphology,
})
}
}
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
pub struct Word { pub struct Word {
id: Uuid, id: Uuid,
@ -229,5 +302,5 @@ pub struct WordLearning {
pub id: i32, pub id: i32,
pub word: Uuid, pub word: Uuid,
pub userid: String, pub userid: String,
pub status: WordLearningStatus pub status: WordLearningStatus,
} }

View File

@ -27,35 +27,42 @@ impl Default for OtherEnvVar {
pub struct Context { pub struct Context {
pub db: Database, pub db: Database,
pub appwrite: APVariables, pub appwrite: APVariables,
pub user_auth: bool, pub user_auth: Option<String>,
pub other_vars: OtherEnvVar, pub other_vars: OtherEnvVar,
} }
impl Context { impl Context {
/// HTTP header for a user's session /// Check if a request is performed by an autentificated user.
/// ///
/// This header `Authorization` must be a single string in the /// The HTTP header `Authorization` must be a single string in the
/// form `userId;userSessionId` with `userId` and `userSessionId` /// form `userId;userSessionId` with `userId` and `userSessionId`
/// being variables given by Appwrite to users that are logged in. /// being variables given by Appwrite to users that are logged in.
pub async fn user_auth<'r>(&self, auth_token: Option<&'r str>) -> bool { ///
/// The function returns either the user's ID if the user is
/// authentified or `None`.
pub async fn user_auth<'r>(
&self,
auth_token: Option<&'r str>,
) -> Option<String> {
if let Some(token) = auth_token { if let Some(token) = auth_token {
let key = token.split(';').collect::<Vec<_>>(); let key = token.split(';').collect::<Vec<_>>();
if key.len() == 2 { if key.len() == 2 {
let user_id = key[0]; let user_id = key[0];
let session_id = key[1]; let session_id = key[1];
match self.appwrite.check_session(session_id, user_id).await { match self.appwrite.check_session(session_id, user_id).await {
Ok(val) => val, Ok(true) => Some(key[0].to_string()),
Ok(false) => None,
Err(e) => { Err(e) => {
info!("Error checking user session: {:?}", e); info!("Error checking user session: {:?}", e);
false None
} }
} }
} else { } else {
info!("Invalid session key: {}", token); info!("Invalid session key: {}", token);
false None
} }
} else { } else {
false None
} }
} }

View File

@ -1,6 +1,15 @@
use juniper::FieldResult; use std::str::FromStr;
use crate::db::{models::users::User, DatabaseError}; use juniper::FieldResult;
use uuid::Uuid;
use crate::db::{
models::{
languages::{Language, NewLanguage, UserFollowLanguage},
users::User,
},
DatabaseError,
};
use super::Context; use super::Context;
@ -9,7 +18,7 @@ pub struct Mutation;
#[juniper::graphql_object(Context = Context)] #[juniper::graphql_object(Context = Context)]
impl Mutation { impl Mutation {
fn api_version(context: &Context) -> String { fn api_version(context: &Context) -> String {
if context.user_auth { if context.user_auth.is_some() {
"0.1 (authentified)" "0.1 (authentified)"
} else { } else {
"0.1 (not authentified)" "0.1 (not authentified)"
@ -49,4 +58,103 @@ impl Mutation {
.into()) .into())
} }
} }
pub fn user_follow_language(
context: &Context,
language: String,
) -> FieldResult<Language> {
if let Some(userid) = &context.user_auth {
match Uuid::from_str(&language) {
Err(e) => Err(DatabaseError::new(
format!(
"Could not parse {language} as a valid UUID: {e:?}"
),
"Bad Request",
)
.into()),
Ok(lang) => UserFollowLanguage::user_follow_language(
context,
&userid.to_string(),
lang,
)
.map_err(Into::into),
}
} else {
Err(DatabaseError::new(
"User not authentificated, cannot proceed",
"Unauthorized",
)
.into())
}
}
pub fn user_unfollow_language(
context: &Context,
language: String,
) -> FieldResult<Language> {
if let Some(userid) = &context.user_auth {
match Uuid::from_str(&language) {
Err(e) => Err(DatabaseError::new(
format!(
"Could not parse {language} as a valid UUID: {e:?}"
),
"Bad Request",
)
.into()),
Ok(lang) => UserFollowLanguage::user_unfollow_language(
context,
&userid.to_string(),
lang,
)
.map_err(Into::into),
}
} else {
Err(DatabaseError::new(
"User not authentificated, cannot proceed",
"Unauthorized",
)
.into())
}
}
pub fn new_language(
context: &Context,
language: NewLanguage,
) -> FieldResult<Language> {
if let Some(owner) = &context.user_auth {
language.insert_db(&context.db, owner).map_err(Into::into)
} else {
Err(DatabaseError::new(
"User not authentificated, cannot create new language",
"Unauthorized",
)
.into())
}
}
pub fn delete_language(
context: &Context,
language: String,
) -> FieldResult<Option<Language>> {
if context.user_auth.is_some() {
match Uuid::from_str(&language) {
Ok(uuid) => Language::delete(context, uuid)
.map(|_| None)
.map_err(Into::into),
Err(e) => Err(DatabaseError::new(
format!(
"Could not parse {language} as a valid UUID: {e:?}"
),
"Bad Request",
)
.into()),
}
} else {
Err(DatabaseError::new(
"User not authentificated, cannot create new language",
"Unauthorized",
)
.into())
}
}
} }