diff --git a/Cargo.toml b/Cargo.toml index dcf6409..59d1533 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,9 +16,11 @@ publish = false dotenvy = "0.15" # Database -diesel = { version = "2.0", features = ["postgres", "chrono", "r2d2"] } +diesel = { version = "2.0.2", features = ["postgres", "chrono", "r2d2", "uuid"] } diesel-derive-enum = { version = "2.0.0-rc.0", features = ["postgres"] } +# DB types chrono = "0.4.23" +uuid = { version = "1.2.2", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } # Web server rocket = "0.5.0-rc.2" diff --git a/migrations/2023-01-03-134426_create_language/down.sql b/migrations/2023-01-03-134426_create_language/down.sql index 135c57d..d8f8cfa 100644 --- a/migrations/2023-01-03-134426_create_language/down.sql +++ b/migrations/2023-01-03-134426_create_language/down.sql @@ -1,6 +1,8 @@ -- This file should undo anything in `up.sql` DROP TABLE LangAndAgents; +DROP TABLE LangTranslatesTo; DROP TABLE Languages; DROP TYPE Release; DROP TYPE DictGenre; DROP TYPE AgentLanguageRelation; +DROP EXTENSION "uuid-ossp"; diff --git a/migrations/2023-01-03-134426_create_language/up.sql b/migrations/2023-01-03-134426_create_language/up.sql index 14c34b1..b32b071 100644 --- a/migrations/2023-01-03-134426_create_language/up.sql +++ b/migrations/2023-01-03-134426_create_language/up.sql @@ -1,13 +1,15 @@ -- Your SQL goes here +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + CREATE TYPE Release as ENUM ('PUBLIC', 'NONCOMMERCIAL', 'RESEARCH', 'PRIVATE'); CREATE TYPE DictGenre as ENUM ('gen', 'lrn', 'ety', 'spe', 'his', 'ort', 'trm'); CREATE TYPE AgentLanguageRelation as ENUM ('publisher', 'author'); CREATE TABLE Languages ( - name VARCHAR(255) PRIMARY KEY, + id UUID DEFAULT uuid_generate_v4 () PRIMARY KEY, + name VARCHAR(255) NOT NULL, native VARCHAR(255), release Release NOT NULL, - targetLanguage TEXT[] NOT NULL, genre DictGenre[] NOT NULL, abstract TEXT, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, @@ -21,6 +23,20 @@ CREATE TABLE Languages ( NOT NULL ); +CREATE TABLE LangTranslatesTo ( + id SERIAL PRIMARY KEY, + langfrom UUID + REFERENCES Languages(id) + ON UPDATE CASCADE + ON DELETE CASCADE + NOT NULL, + langto UUID + REFERENCES Languages(id) + ON UPDATE CASCADE + ON DELETE CASCADE + NOT NULL +); + CREATE TABLE LangAndAgents ( id SERIAL PRIMARY KEY, agent VARCHAR(31) @@ -28,8 +44,8 @@ CREATE TABLE LangAndAgents ( ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, - language VARCHAR(255) - REFERENCES Languages(name) + language UUID + REFERENCES Languages(id) ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, diff --git a/migrations/2023-01-03-134434_create_word/up.sql b/migrations/2023-01-03-134434_create_word/up.sql index a8dd2fe..de56b8f 100644 --- a/migrations/2023-01-03-134434_create_word/up.sql +++ b/migrations/2023-01-03-134434_create_word/up.sql @@ -9,8 +9,8 @@ CREATE TABLE Words ( REFERENCES Words(norm) ON UPDATE CASCADE ON DELETE SET NULL, - language VARCHAR(255) - REFERENCES Languages(name) + language UUID + REFERENCES Languages(id) ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, diff --git a/src/db/mod.rs b/src/db/mod.rs index 267d670..86e1428 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -119,7 +119,7 @@ impl Database { } } - pub fn words(&self, language: &str, word: &str) -> Vec { + pub fn words(&self, language: uuid::Uuid, word: &str) -> Vec { use self::schema::words::dsl; if let Ok(conn) = &mut self.conn() { match dsl::words diff --git a/src/db/models/languages.rs b/src/db/models/languages.rs index 2922b87..f698ca3 100644 --- a/src/db/models/languages.rs +++ b/src/db/models/languages.rs @@ -3,10 +3,12 @@ use diesel::prelude::*; use juniper::GraphQLEnum; use tracing::info; +use uuid::Uuid; + use super::super::schema; use super::users::User; -use schema::{langandagents, languages}; +use schema::{langandagents, langtranslatesto, languages}; #[derive( diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, @@ -43,12 +45,12 @@ pub enum AgentLanguageRelation { Author, } -#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] +#[derive(Queryable, Insertable, Debug, Clone)] pub struct Language { + id: Uuid, name: String, native: Option, release: Release, - targetlanguage: Vec>, genre: Vec>, abstract_: Option, created: chrono::NaiveDateTime, @@ -67,7 +69,7 @@ impl Language { use schema::langandagents::dsl; match &mut context.conn() { Ok(conn) => dsl::langandagents - .filter(dsl::language.eq(self.name.clone())) + .filter(dsl::language.eq(self.id)) .filter(dsl::relationship.eq(relationship)) .load::(conn) .unwrap() @@ -97,6 +99,11 @@ impl Language { #[juniper::graphql_object(Context = Database)] impl Language { + #[graphql(description = "Unique identifier of the language")] + fn id(&self) -> String { + self.id.to_string() + } + #[graphql( description = "Name in the main target language (often English) of the described language" )] @@ -119,23 +126,16 @@ impl Language { description = "Languages in which the current language is translated" )] fn target_language(&self, context: &Database) -> Vec { - use schema::languages::dsl; + use schema::langtranslatesto::dsl; match &mut context.conn() { - Ok(conn) => self - .targetlanguage - .clone() + Ok(conn) => dsl::langtranslatesto + .filter(dsl::langfrom.eq(self.id)) + .load::(conn) + .unwrap() .into_iter() - .flatten() - .map(|l| dsl::languages.find(l).first::(conn)) - .filter_map(|l| match l { - Ok(language) => Some(language), - Err(e) => { - info!( - "Failed to retrieve language from database: {:?}", - e - ); - None - } + .flat_map(|l| { + use schema::languages::dsl; + dsl::languages.find(l.langto).first::(conn) }) .collect::>(), Err(e) => { @@ -223,6 +223,14 @@ impl Language { pub struct LangAndAgent { id: i32, agent: String, - language: String, + language: Uuid, relationship: AgentLanguageRelation, } + +#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] +#[diesel(table_name = langtranslatesto)] +pub struct LangTranslatesTo { + id: i32, + langfrom: Uuid, + langto: Uuid, +} diff --git a/src/db/models/words.rs b/src/db/models/words.rs index a7d0bf5..a7b876f 100644 --- a/src/db/models/words.rs +++ b/src/db/models/words.rs @@ -14,7 +14,9 @@ pub enum WordRelationship { Related, } -#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum)] +#[derive( + diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum, +)] #[DieselTypePath = "crate::db::schema::sql_types::Partofspeech"] pub enum PartOfSpeech { Adjective, @@ -44,7 +46,7 @@ pub struct Word { norm: String, native: Option, lemma: Option, - language: String, + language: uuid::Uuid, partofspeech: PartOfSpeech, audio: Option, video: Option, @@ -56,29 +58,30 @@ pub struct Word { } impl Word { - fn relationship(&self, context: &Database, relationship: WordRelationship) -> Vec { + fn relationship( + &self, + context: &Database, + relationship: WordRelationship, + ) -> Vec { use schema::wordrelation::dsl; match &mut context.conn() { - Ok(conn) => { - dsl::wordrelation - .filter(dsl::wordsource.eq(self.norm.clone())) - .filter(dsl::relationship.eq(relationship)) - .load::(conn) - .unwrap() - .into_iter() - .flat_map(|w| { - use schema::words::dsl; - dsl::words.find(w.wordtarget).first::(conn) - }) - .collect::>() - }, + Ok(conn) => dsl::wordrelation + .filter(dsl::wordsource.eq(self.norm.clone())) + .filter(dsl::relationship.eq(relationship)) + .load::(conn) + .unwrap() + .into_iter() + .flat_map(|w| { + use schema::words::dsl; + dsl::words.find(w.wordtarget).first::(conn) + }) + .collect::>(), Err(e) => { info!("Could not connect to database: {:?}", e); Vec::new() } } } - } #[juniper::graphql_object(Context = Database)] @@ -98,16 +101,18 @@ impl Word { use schema::words::dsl; match self.lemma.clone() { Some(lemma) => match &mut context.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.clone()).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 @@ -122,9 +127,7 @@ impl Word { use schema::languages::dsl; match &mut context.conn() { Ok(conn) => { - match dsl::languages - .find(self.language.clone()) - .first::(conn) + match dsl::languages.find(self.language).first::(conn) { Ok(lang) => lang, Err(e) => { @@ -138,7 +141,10 @@ impl Word { } } - #[graphql(name = "partOfSpeech", description = "Part of speech the word belongs to")] + #[graphql( + name = "partOfSpeech", + description = "Part of speech the word belongs to" + )] fn part_of_speech(&self) -> PartOfSpeech { self.partofspeech.clone() } @@ -168,18 +174,25 @@ impl Word { self.lusage.clone() } - - #[graphql(description = "Morphology of the word, can be in Markdown format")] + #[graphql( + description = "Morphology of the word, can be in Markdown format" + )] fn morphology(&self) -> Option { self.morphology.clone() } - #[graphql(name = "related", description = "Words related to the current word")] + #[graphql( + name = "related", + description = "Words related to the current word" + )] fn related_words(&self, context: &Database) -> Vec { self.relationship(context, WordRelationship::Related) } - #[graphql(name = "definitions", description = "Words that define the current word")] + #[graphql( + name = "definitions", + description = "Words that define the current word" + )] fn definitions(&self, context: &Database) -> Vec { self.relationship(context, WordRelationship::Definition) } diff --git a/src/db/schema.rs b/src/db/schema.rs index 8ca71e4..a3147d9 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -29,21 +29,29 @@ diesel::table! { langandagents (id) { id -> Int4, agent -> Varchar, - language -> Varchar, + language -> Uuid, relationship -> Agentlanguagerelation, } } +diesel::table! { + langtranslatesto (id) { + id -> Int4, + langfrom -> Uuid, + langto -> Uuid, + } +} + diesel::table! { use diesel::sql_types::*; use super::sql_types::Release; use super::sql_types::Dictgenre; - languages (name) { + languages (id) { + id -> Uuid, name -> Varchar, native -> Nullable, release -> Release, - targetlanguage -> Array>, genre -> Array>, #[sql_name = "abstract"] abstract_ -> Nullable, @@ -90,7 +98,7 @@ diesel::table! { norm -> Varchar, native -> Nullable, lemma -> Nullable, - language -> Varchar, + language -> Uuid, partofspeech -> Partofspeech, audio -> Nullable, video -> Nullable, @@ -109,6 +117,7 @@ diesel::joinable!(words -> languages (language)); diesel::allow_tables_to_appear_in_same_query!( langandagents, + langtranslatesto, languages, userfollows, users, diff --git a/src/graphql.rs b/src/graphql.rs index ae28681..1b2a8a5 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -7,6 +7,8 @@ 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; @@ -21,7 +23,11 @@ impl Query { } #[graphql( - description = "Retrieve a specific language from its name and its owner's id" + 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, @@ -31,21 +37,38 @@ impl Query { context.language(name.as_str(), owner.as_str()) } - #[graphql(description = "Retrieve a specific user from its id")] + #[graphql( + description = "Retrieve a specific user from its id", + arguments(id(description = "Appwrite ID of a user")) + )] fn user(context: &Database, id: String) -> Option { context.user(id.as_str()) } - #[graphql(description = "Retrieve a specific word from its id")] + #[graphql( + description = "Retrieve a specific word from its id", + arguments(id(description = "Unique identifier of a word")) + )] fn word(context: &Database, id: String) -> Option { context.word_id(id.as_str()) } #[graphql( - description = "Retrieve all words with a set normal form from a set language" + 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 { - context.words(language.as_str(), word.as_str()) + fn words( + context: &Database, + language: String, + word: String, + ) -> Vec { + context.words(uuid::Uuid::from_str(&language).unwrap(), word.as_str()) } }