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:
parent
51f0fc3108
commit
a624636939
@ -27,6 +27,12 @@ pub enum Release {
|
||||
Private,
|
||||
}
|
||||
|
||||
impl Default for Release {
|
||||
fn default() -> Self {
|
||||
Self::Public
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum,
|
||||
)]
|
||||
@ -50,6 +56,91 @@ pub enum AgentLanguageRelation {
|
||||
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)]
|
||||
pub struct Language {
|
||||
id: Uuid,
|
||||
@ -66,6 +157,59 @@ pub struct 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(
|
||||
&self,
|
||||
db: &Database,
|
||||
@ -290,6 +434,13 @@ pub struct LangTranslatesTo {
|
||||
langto: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = userfollowlanguage)]
|
||||
pub struct UserFollowLanguageInsert {
|
||||
pub lang: Uuid,
|
||||
pub userid: String,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
|
||||
#[diesel(table_name = userfollowlanguage)]
|
||||
pub struct UserFollowLanguage {
|
||||
@ -297,3 +448,71 @@ pub struct UserFollowLanguage {
|
||||
pub lang: Uuid,
|
||||
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",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,11 @@ use crate::{
|
||||
};
|
||||
use diesel::prelude::*;
|
||||
use juniper::{FieldResult, GraphQLEnum};
|
||||
use schema::{wordrelation, words, wordlearning};
|
||||
use schema::{wordlearning, wordrelation, words};
|
||||
use tracing::info;
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::convert::Into;
|
||||
use std::{convert::Into, str::FromStr};
|
||||
|
||||
use super::languages::Language;
|
||||
|
||||
@ -20,11 +20,18 @@ pub enum WordRelationship {
|
||||
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"]
|
||||
pub enum WordLearningStatus {
|
||||
Learning,
|
||||
Learned
|
||||
Learned,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
@ -54,6 +61,72 @@ pub enum PartOfSpeech {
|
||||
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)]
|
||||
pub struct Word {
|
||||
id: Uuid,
|
||||
@ -229,5 +302,5 @@ pub struct WordLearning {
|
||||
pub id: i32,
|
||||
pub word: Uuid,
|
||||
pub userid: String,
|
||||
pub status: WordLearningStatus
|
||||
pub status: WordLearningStatus,
|
||||
}
|
||||
|
@ -27,35 +27,42 @@ impl Default for OtherEnvVar {
|
||||
pub struct Context {
|
||||
pub db: Database,
|
||||
pub appwrite: APVariables,
|
||||
pub user_auth: bool,
|
||||
pub user_auth: Option<String>,
|
||||
pub other_vars: OtherEnvVar,
|
||||
}
|
||||
|
||||
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`
|
||||
/// 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 {
|
||||
let key = token.split(';').collect::<Vec<_>>();
|
||||
if key.len() == 2 {
|
||||
let user_id = key[0];
|
||||
let session_id = key[1];
|
||||
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) => {
|
||||
info!("Error checking user session: {:?}", e);
|
||||
false
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Invalid session key: {}", token);
|
||||
false
|
||||
None
|
||||
}
|
||||
} else {
|
||||
false
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
@ -9,7 +18,7 @@ pub struct Mutation;
|
||||
#[juniper::graphql_object(Context = Context)]
|
||||
impl Mutation {
|
||||
fn api_version(context: &Context) -> String {
|
||||
if context.user_auth {
|
||||
if context.user_auth.is_some() {
|
||||
"0.1 (authentified)"
|
||||
} else {
|
||||
"0.1 (not authentified)"
|
||||
@ -49,4 +58,103 @@ impl Mutation {
|
||||
.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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user