diff --git a/migrations/2023-01-03-134426_create_language/down.sql b/migrations/2023-01-03-134426_create_language/down.sql index d8f8cfa..3b2f058 100644 --- a/migrations/2023-01-03-134426_create_language/down.sql +++ b/migrations/2023-01-03-134426_create_language/down.sql @@ -1,4 +1,5 @@ -- This file should undo anything in `up.sql` +DROP TABLE UserFollowLanguage; DROP TABLE LangAndAgents; DROP TABLE LangTranslatesTo; DROP TABLE Languages; diff --git a/migrations/2023-01-03-134426_create_language/up.sql b/migrations/2023-01-03-134426_create_language/up.sql index b32b071..861c1ba 100644 --- a/migrations/2023-01-03-134426_create_language/up.sql +++ b/migrations/2023-01-03-134426_create_language/up.sql @@ -51,3 +51,17 @@ CREATE TABLE LangAndAgents ( NOT NULL, relationship AgentLanguageRelation NOT NULL ); + +CREATE TABLE UserFollowLanguage ( + id SERIAL PRIMARY KEY, + lang UUID + REFERENCES Languages(id) + ON UPDATE CASCADE + ON DELETE CASCADE + NOT NULL, + userid VARCHAR(31) + REFERENCES Users(id) + ON UPDATE CASCADE + ON DELETE CASCADE + NOT NULL +); diff --git a/migrations/2023-01-03-134434_create_word/down.sql b/migrations/2023-01-03-134434_create_word/down.sql index 291a97c..6972165 100644 --- a/migrations/2023-01-03-134434_create_word/down.sql +++ b/migrations/2023-01-03-134434_create_word/down.sql @@ -1 +1,7 @@ --- This file should undo anything in `up.sql` \ No newline at end of file +-- This file should undo anything in `up.sql` +DROP TABLE WordRelation; +DROP TABLE WordLearning; +DROP TABLE Words; +DROP TYPE WordLearningStatus; +DROP TYPE WordRelationship; +DROP TYPE PartOfSpeech; diff --git a/migrations/2023-01-03-134434_create_word/up.sql b/migrations/2023-01-03-134434_create_word/up.sql index de56b8f..a2996c6 100644 --- a/migrations/2023-01-03-134434_create_word/up.sql +++ b/migrations/2023-01-03-134434_create_word/up.sql @@ -1,12 +1,14 @@ -- Your SQL goes here CREATE TYPE PartOfSpeech as ENUM ('ADJ', 'ADP', 'ADV', 'AUX', 'CCONJ', 'DET', 'INTJ', 'NOUN', 'NUM', 'PART', 'PRON', 'PROPN', 'PUNCT', 'SCONJ', 'SYM', 'VERB', 'X'); CREATE TYPE WordRelationship as ENUM('def', 'related'); +CREATE TYPE WordLearningStatus as ENUM('learning', 'learned'); CREATE TABLE Words ( - norm VARCHAR(255) PRIMARY KEY, -- normalized word + id UUID DEFAULT uuid_generate_v4 () PRIMARY KEY, + norm VARCHAR(255) NOT NULL, -- normalized word, generally in latin alphabet native VARCHAR(255), - lemma VARCHAR(255) - REFERENCES Words(norm) + lemma UUID + REFERENCES Words(id) ON UPDATE CASCADE ON DELETE SET NULL, language UUID @@ -26,15 +28,30 @@ CREATE TABLE Words ( CREATE TABLE WordRelation ( id SERIAL PRIMARY KEY, - wordsource VARCHAR(255) - REFERENCES Words(norm) + wordsource UUID + REFERENCES Words(id) ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, - wordtarget VARCHAR(255) - REFERENCES Words(norm) + wordtarget UUID + REFERENCES Words(id) ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, relationship WordRelationship NOT NULL ); + +CREATE TABLE WordLearning ( + id SERIAL PRIMARY KEY, + word UUID + REFERENCES Words(id) + ON UPDATE CASCADE + ON DELETE CASCADE + NOT NULL, + userid VARCHAR(31) + REFERENCES Users(id) + ON UPDATE CASCADE + ON DELETE CASCADE + NOT NULL, + status WordLearningStatus DEFAULT 'learning' NOT NULL +); diff --git a/src/db/mod.rs b/src/db/mod.rs index 8d0a9ec..555ed3c 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -219,7 +219,7 @@ impl Database { } } - pub fn word_id(&self, id: &str) -> Result, DatabaseError> { + pub fn word_id(&self, id: uuid::Uuid) -> Result, DatabaseError> { use self::schema::words::dsl; match dsl::words.find(id).first::(&mut self.conn()?) { Ok(val) => Ok(Some(val)), diff --git a/src/db/models/languages.rs b/src/db/models/languages.rs index b439ba7..2a74018 100644 --- a/src/db/models/languages.rs +++ b/src/db/models/languages.rs @@ -13,7 +13,7 @@ use super::users::User; use std::convert::Into; -use schema::{langandagents, langtranslatesto, languages}; +use schema::{langandagents, langtranslatesto, languages, userfollowlanguage}; #[derive( diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, @@ -209,7 +209,7 @@ impl Language { "Failed to retrieve owner {} of language {}: {e:?}", self.owner, self.name ), - "Database reading error", + "Database error", ) })?), Err(e) => Err(DatabaseError::new( @@ -235,6 +235,42 @@ impl Language { self.relationship(&context.db, AgentLanguageRelation::Publisher) .map_err(Into::into) } + + #[graphql(description = "People following the language")] + fn followers(&self, context: &Context) -> FieldResult> { + use schema::userfollowlanguage::dsl; + match &mut context.db.conn() { + Ok(conn) => { + Ok(dsl::userfollowlanguage + .filter(dsl::lang.eq(self.id)) + .load::(conn) + .map_err(|e| { + DatabaseError::new(format!("Failed to retrieve language followers for language {}: {e:?}", self.id), + "Database error") + })? + .into_iter() + .filter_map(|follow| { + use schema::users::dsl; + match dsl::users + .find(follow.userid.clone()) + .first::(conn) { + Ok(user) => Some(user), + Err(e) => { + info!("Failed to retrieve user {} from database: {e:?}", follow.userid); + None + } + } + }) + .collect::>() + ) + } + Err(e) => Err(DatabaseError::new( + format!("Failed to connect to the database: {e:?}"), + "Database connection failure", + ) + .into()), + } + } } #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] @@ -253,3 +289,11 @@ pub struct LangTranslatesTo { langfrom: Uuid, langto: Uuid, } + +#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] +#[diesel(table_name = userfollowlanguage)] +pub struct UserFollowLanguage { + id: i32, + lang: Uuid, + userid: String, +} diff --git a/src/db/models/users.rs b/src/db/models/users.rs index ed85325..976c1e4 100644 --- a/src/db/models/users.rs +++ b/src/db/models/users.rs @@ -1,7 +1,12 @@ -use super::super::schema::{userfollows, users}; +use super::{ + super::schema, + words::{Word, WordLearning, WordLearningStatus}, +}; use diesel::prelude::*; use juniper::FieldResult; -use tracing::debug; +use tracing::{debug, info}; + +use schema::{userfollows, users}; use crate::{db::DatabaseError, graphql::Context}; @@ -33,8 +38,8 @@ impl User { ) })?; Ok(userfollows::dsl::userfollows - .filter(userfollows::dsl::follower.eq(self.id.clone())) - .load::(conn) + .filter(userfollows::dsl::follower.eq(self.id.clone())) + .load::(conn) .map_err(|e| { DatabaseError::new( format!( @@ -62,6 +67,53 @@ impl User { }) .collect::>()) } + + #[graphql( + description = "What words the user is learning or has learned", + arguments(status( + description = "Display either words being learned or words learned" + )) + )] + pub fn words_learning( + &self, + context: &Context, + status: WordLearningStatus, + ) -> FieldResult> { + use schema::wordlearning::dsl; + let conn = &mut context.db.conn().map_err(|e| { + DatabaseError::new( + format!("Failed to connect to database: {e:?}"), + "Database connection error", + ) + })?; + Ok(dsl::wordlearning + .filter(dsl::userid.eq(self.id.clone())) + .filter(dsl::status.eq(status)) + .load::(conn) + .map_err(|e| { + DatabaseError::new( + format!( + "Failed to retrieve user follows from database: {e:?}" + ), + "Database reading error", + ) + })? + .iter() + .filter_map(|lang_learn| { + use schema::words::dsl; + match dsl::words.find(lang_learn.word).first::(conn) { + Ok(word) => Some(word), + Err(e) => { + info!( + "Failed to retrieve word {} from database: {e:?}", + lang_learn.word + ); + None + } + } + }) + .collect::>()) + } } #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] diff --git a/src/db/models/words.rs b/src/db/models/words.rs index 4ccc070..6df2087 100644 --- a/src/db/models/words.rs +++ b/src/db/models/words.rs @@ -5,8 +5,9 @@ use crate::{ }; use diesel::prelude::*; use juniper::{FieldResult, GraphQLEnum}; -use schema::{wordrelation, words}; +use schema::{wordrelation, words, wordlearning}; use tracing::info; +use uuid::Uuid; use std::convert::Into; @@ -19,6 +20,13 @@ pub enum WordRelationship { Related, } +#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, juniper::GraphQLEnum)] +#[DieselTypePath = "crate::db::schema::sql_types::Wordlearningstatus"] +pub enum WordLearningStatus { + Learning, + Learned +} + #[derive( diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, )] @@ -48,9 +56,10 @@ pub enum PartOfSpeech { #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] pub struct Word { + id: Uuid, norm: String, native: Option, - lemma: Option, + lemma: Option, language: uuid::Uuid, partofspeech: PartOfSpeech, audio: Option, @@ -71,7 +80,7 @@ impl Word { use schema::wordrelation::dsl; match &mut db.conn() { Ok(conn) => Ok(dsl::wordrelation - .filter(dsl::wordsource.eq(self.norm.clone())) + .filter(dsl::wordsource.eq(self.id)) .filter(dsl::relationship.eq(relationship)) .load::(conn) .map_err(|e| { @@ -81,9 +90,9 @@ impl Word { ) })? .into_iter() - .flat_map(|w| { + .flat_map(|word| { use schema::words::dsl; - dsl::words.find(w.wordtarget).first::(conn) + dsl::words.find(word.wordtarget).first::(conn) }) .collect::>()), Err(e) => Err(DatabaseError::new( @@ -109,20 +118,18 @@ impl Word { #[graphql(description = "Base form of the current word")] fn lemma(&self, context: &Context) -> Option { use schema::words::dsl; - match self.lemma.clone() { + match self.lemma { Some(lemma) => match &mut context.db.conn() { - Ok(conn) => { - match dsl::words.find(lemma.clone()).first::(conn) { - Ok(word) => Some(word), - Err(e) => { - info!( - "Failed to retrieve lemma {} of word {}: {:?}", - lemma, self.norm, e - ); - None - } + Ok(conn) => match dsl::words.find(lemma).first::(conn) { + Ok(word) => Some(word), + Err(e) => { + info!( + "Failed to retrieve lemma {} of word {}: {:?}", + lemma, self.norm, e + ); + None } - } + }, Err(e) => { info!("Could not connect to the database: {:?}", e); None @@ -211,7 +218,16 @@ impl Word { #[diesel(table_name = wordrelation)] pub struct WordRelation { id: i32, - wordsource: String, - wordtarget: String, + wordsource: Uuid, + wordtarget: Uuid, relationship: WordRelationship, } + +#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] +#[diesel(table_name = wordlearning)] +pub struct WordLearning { + pub id: i32, + pub word: Uuid, + pub userid: String, + pub status: WordLearningStatus +} diff --git a/src/db/schema.rs b/src/db/schema.rs index a3147d9..9d9305e 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -17,6 +17,10 @@ pub mod sql_types { #[diesel(postgres_type(name = "release"))] pub struct Release; + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "wordlearningstatus"))] + pub struct Wordlearningstatus; + #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "wordrelationship"))] pub struct Wordrelationship; @@ -63,6 +67,14 @@ diesel::table! { } } +diesel::table! { + userfollowlanguage (id) { + id -> Int4, + lang -> Uuid, + userid -> Varchar, + } +} + diesel::table! { userfollows (id) { id -> Int4, @@ -78,14 +90,26 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use super::sql_types::Wordlearningstatus; + + wordlearning (id) { + id -> Int4, + word -> Uuid, + userid -> Varchar, + status -> Wordlearningstatus, + } +} + diesel::table! { use diesel::sql_types::*; use super::sql_types::Wordrelationship; wordrelation (id) { id -> Int4, - wordsource -> Varchar, - wordtarget -> Varchar, + wordsource -> Uuid, + wordtarget -> Uuid, relationship -> Wordrelationship, } } @@ -94,10 +118,11 @@ diesel::table! { use diesel::sql_types::*; use super::sql_types::Partofspeech; - words (norm) { + words (id) { + id -> Uuid, norm -> Varchar, native -> Nullable, - lemma -> Nullable, + lemma -> Nullable, language -> Uuid, partofspeech -> Partofspeech, audio -> Nullable, @@ -113,14 +138,20 @@ diesel::table! { diesel::joinable!(langandagents -> languages (language)); diesel::joinable!(langandagents -> users (agent)); diesel::joinable!(languages -> users (owner)); +diesel::joinable!(userfollowlanguage -> languages (lang)); +diesel::joinable!(userfollowlanguage -> users (userid)); +diesel::joinable!(wordlearning -> users (userid)); +diesel::joinable!(wordlearning -> words (word)); diesel::joinable!(words -> languages (language)); diesel::allow_tables_to_appear_in_same_query!( langandagents, langtranslatesto, languages, + userfollowlanguage, userfollows, users, + wordlearning, wordrelation, words, ); diff --git a/src/graphql/query.rs b/src/graphql/query.rs index d3056fa..4278248 100644 --- a/src/graphql/query.rs +++ b/src/graphql/query.rs @@ -96,7 +96,14 @@ impl Query { arguments(id(description = "Unique identifier of a word")) )] fn word(context: &Context, id: String) -> FieldResult> { - context.db.word_id(id.as_str()).map_err(Into::into) + match Uuid::from_str(&id) { + Ok(uuid) => context.db.word_id(uuid).map_err(Into::into), + Err(e) => Err(DatabaseError::new( + format!("Failed to convert {id} to a UUID: {e:?}"), + "Conversion Error", + ) + .into()), + } } #[graphql(