ordabok/src/db/models/words.rs
Lucien Cartier-Tilet c5f5e770e2
All checks were successful
continuous-integration/drone/push Build is passing
Fix word name collision, add two new user-related features
This commit changes the primary key of words to a serial number. That
way, two words with the same normalized value will not collide with
one another.

It also adds two new tables in the database:
- Users following languages
- Users learning words

The former can represent two stages of learning a word:
- Either the user is currently learning it
- Or they consider they know it and don’t need to work on it anymore

These two new tables now have their API query available through the
GraphQL API.

This commit also fixes the issue of word-related tables and types not
being dropped when resetting the database.
2023-01-18 10:26:45 +01:00

234 lines
6.6 KiB
Rust

use super::super::schema;
use crate::{
db::{Database, DatabaseError},
graphql::Context,
};
use diesel::prelude::*;
use juniper::{FieldResult, GraphQLEnum};
use schema::{wordrelation, words, wordlearning};
use tracing::info;
use uuid::Uuid;
use std::convert::Into;
use super::languages::Language;
#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq)]
#[DieselTypePath = "crate::db::schema::sql_types::Wordrelationship"]
pub enum WordRelationship {
Definition,
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,
)]
#[DieselTypePath = "crate::db::schema::sql_types::Partofspeech"]
pub enum PartOfSpeech {
Adjective,
Adposition,
Adverb,
Auxilliary,
#[graphql(name = "COORDINATING_CONJUNCTION")]
CoordConj,
Determiner,
Interjection,
Noun,
Numeral,
Particle,
Pronoun,
#[graphql(name = "PROPER_NOUN")]
ProperNoun,
Punctuation,
#[graphql(name = "SUBORDINATING_CONJUNCTION")]
SubjConj,
Symbol,
Verb,
Other,
}
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
pub struct Word {
id: Uuid,
norm: String,
native: Option<String>,
lemma: Option<Uuid>,
language: uuid::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 Word {
fn relationship(
&self,
db: &Database,
relationship: WordRelationship,
) -> Result<Vec<Word>, DatabaseError> {
use schema::wordrelation::dsl;
match &mut db.conn() {
Ok(conn) => Ok(dsl::wordrelation
.filter(dsl::wordsource.eq(self.id))
.filter(dsl::relationship.eq(relationship))
.load::<WordRelation>(conn)
.map_err(|e| {
DatabaseError::new(
format!("Failed to retrieve word relations: {e:?}"),
"Database reading failed",
)
})?
.into_iter()
.flat_map(|word| {
use schema::words::dsl;
dsl::words.find(word.wordtarget).first::<Word>(conn)
})
.collect::<Vec<Word>>()),
Err(e) => Err(DatabaseError::new(
format!("Failed to connect to the database: {e:?}"),
"Database connection error",
)),
}
}
}
#[juniper::graphql_object(Context = Context)]
impl Word {
#[graphql(description = "Normal form of the word")]
fn norm(&self) -> String {
self.norm.clone()
}
#[graphql(description = "Native representation of the word")]
fn native(&self) -> Option<String> {
self.native.clone()
}
#[graphql(description = "Base form of the current word")]
fn lemma(&self, context: &Context) -> Option<Word> {
use schema::words::dsl;
match self.lemma {
Some(lemma) => match &mut context.db.conn() {
Ok(conn) => match dsl::words.find(lemma).first::<Word>(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
}
},
None => None,
}
}
#[graphql(description = "Language to which the word belongs")]
fn language(&self, context: &Context) -> FieldResult<Language> {
use schema::languages::dsl;
use std::convert::Into;
dsl::languages
.find(self.language)
.first::<Language>(&mut context.db.conn()?)
.map_err(|e| DatabaseError::new(
format!(
"Failed to retrieve language {} of word {} from database: {e:?}",
self.language, self.norm
),
"Database Error"
).into())
}
#[graphql(
name = "partOfSpeech",
description = "Part of speech the word belongs to"
)]
fn part_of_speech(&self) -> PartOfSpeech {
self.partofspeech.clone()
}
#[graphql(description = "Link to an audio file related to the word")]
fn audio(&self) -> Option<String> {
self.audio.clone()
}
#[graphql(description = "Link to an video file related to the word")]
fn video(&self) -> Option<String> {
self.video.clone()
}
#[graphql(description = "Link to an image file related to the word")]
fn image(&self) -> Option<String> {
self.image.clone()
}
#[graphql(description = "Etymology of the word, can be in Markdown format")]
fn etymology(&self) -> Option<String> {
self.etymology.clone()
}
#[graphql(description = "Usage of the word, can be in Markdown format")]
fn usage(&self) -> Option<String> {
self.lusage.clone()
}
#[graphql(
description = "Morphology of the word, can be in Markdown format"
)]
fn morphology(&self) -> Option<String> {
self.morphology.clone()
}
#[graphql(
name = "related",
description = "Words related to the current word"
)]
fn related_words(&self, context: &Context) -> FieldResult<Vec<Word>> {
self.relationship(&context.db, WordRelationship::Related)
.map_err(Into::into)
}
#[graphql(
name = "definitions",
description = "Words that define the current word"
)]
fn definitions(&self, context: &Context) -> FieldResult<Vec<Word>> {
self.relationship(&context.db, WordRelationship::Definition)
.map_err(Into::into)
}
}
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
#[diesel(table_name = wordrelation)]
pub struct WordRelation {
id: i32,
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
}