From 8d5e523ab3789af33c7cc2173c551a8fc961b9bd Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Wed, 4 Jan 2023 19:31:52 +0100 Subject: [PATCH] Initial GraphQL API sort of working --- Cargo.toml | 12 ++++++- src/db/mod.rs | 71 ++++++++++++++++++++++++++++++++++++++ src/db/models/languages.rs | 36 ++++++++++++++----- src/db/models/users.rs | 11 ++++++ src/db/models/words.rs | 4 +-- src/graphql.rs | 63 +++++++++++++++++++++++++++++++++ src/main.rs | 22 ++++++++++-- 7 files changed, 206 insertions(+), 13 deletions(-) create mode 100644 src/graphql.rs diff --git a/Cargo.toml b/Cargo.toml index ebb308f..dcf6409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,12 @@ name = "ordabok" version = "0.1.0" edition = "2021" +license = "AGPL-3.0" +authors = ["Lucien Cartier-Tilet "] +homepage = "https://labs.phundrak.com/phundrak/ordabok" +repository = "https://labs.phundrak.com/phundrak/ordabok" +readme = "README.org" +publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,7 +16,7 @@ edition = "2021" dotenvy = "0.15" # Database -diesel = { version = "2.0", features = ["postgres", "chrono"] } +diesel = { version = "2.0", features = ["postgres", "chrono", "r2d2"] } diesel-derive-enum = { version = "2.0.0-rc.0", features = ["postgres"] } chrono = "0.4.23" @@ -18,6 +24,10 @@ chrono = "0.4.23" rocket = "0.5.0-rc.2" rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e814" } +# GraphQL +juniper = "0.15.10" +juniper_rocket = "0.8.2" + # logging tracing = "0.1.37" tracing-subscriber = "0.3.16" \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs index d5cbad7..010dd33 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,2 +1,73 @@ pub mod models; pub mod schema; + +use self::models::languages::Language; +use diesel::pg::PgConnection; +use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; +use dotenvy::dotenv; +use std::env; +use tracing::info; + +use diesel::prelude::*; +// use diesel::query_dsl::RunQueryDsl; + +pub struct Database { + conn: Pool>, +} + +impl juniper::Context for Database {} + +impl Database { + pub fn new() -> Self { + Self { + conn: Database::get_connection_pool(), + } + } + + pub fn get_connection_pool() -> Pool> { + dotenv().ok(); + let database_url = + env::var("DATABASE_URL").expect("DATABASE_URL must be set!"); + info!("Connecting to {}", database_url); + let manager = ConnectionManager::::new(database_url); + Pool::builder() + .test_on_check_out(true) + .build(manager) + .expect("Could not build connection pool") + } + + fn conn( + &self, + ) -> Result>, ()> { + self.conn + .get() + .map_err(|e| info!("Failed to connect to database: {:?}", e)) + } + + pub fn all_languages(&self) -> Result, ()> { + use self::schema::languages::dsl::languages; + languages.load::(&mut self.conn()?).map_err(|e| { + info!("Failed to retrieve languages from database: {:?}", e); + }) + } + + pub fn language(&self, name: &str) -> Option { + use self::schema::languages::dsl::languages; + match &mut self.conn() { + Ok(val) => languages + .find(name.to_string()) + .first::(val) + .map_or_else( + |e| { + info!( + "Failed to retrieve language {} from database: {:?}", + name, e + ); + None + }, + Some, + ), + Err(_) => None, + } + } +} diff --git a/src/db/models/languages.rs b/src/db/models/languages.rs index b9d7152..3b7cb4d 100644 --- a/src/db/models/languages.rs +++ b/src/db/models/languages.rs @@ -1,16 +1,18 @@ use super::super::schema::{langandagents, languages}; use diesel::prelude::*; +use juniper::GraphQLEnum; -#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq)] +#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum)] #[DieselTypePath = "crate::db::schema::sql_types::Release"] pub enum Release { Public, + #[graphql(name="NON_COMMERCIAL")] NonCommercial, Research, Private, } -#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq)] +#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum)] #[DieselTypePath = "crate::db::schema::sql_types::Dictgenre"] pub enum DictGenre { General, @@ -22,7 +24,7 @@ pub enum DictGenre { Terminology, } -#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq)] +#[derive(diesel_derive_enum::DbEnum, Debug, Clone, PartialEq, Eq, GraphQLEnum)] #[DieselTypePath = "crate::db::schema::sql_types::Agentlanguagerelation"] pub enum AgentLanguageRelation { Publisher, @@ -31,17 +33,35 @@ pub enum AgentLanguageRelation { #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] pub struct Language { - release: Release, - created: chrono::NaiveDateTime, name: String, - owner: String, - targetlanguage: Vec, - genre: Vec, native: Option, + release: Release, + targetlanguage: Vec>, + genre: Vec>, abstract_: Option, + created: chrono::NaiveDateTime, description: Option, rights: Option, license: Option, + owner: String, +} + +#[juniper::graphql_object] +impl Language { + #[graphql(name = "release")] + fn release(&self) -> Release { + self.release.clone() + } + + #[graphql(name = "created")] + fn created(&self) -> String { + self.created.to_string() + } + + #[graphql(name = "name")] + fn name(&self) -> String { + self.name.clone() + } } #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] diff --git a/src/db/models/users.rs b/src/db/models/users.rs index 7a9d024..d52b0a5 100644 --- a/src/db/models/users.rs +++ b/src/db/models/users.rs @@ -7,6 +7,17 @@ pub struct User { pub username: String, } +#[juniper::graphql_object] +impl User { + pub fn id(&self) -> &str { + self.id.as_str() + } + + pub fn username(&self) -> &str { + self.username.as_str() + } +} + #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[diesel(table_name = userfollows)] pub struct UserFollow { diff --git a/src/db/models/words.rs b/src/db/models/words.rs index a0d330d..ecba2d0 100644 --- a/src/db/models/words.rs +++ b/src/db/models/words.rs @@ -31,7 +31,7 @@ pub enum PartOfSpeech { } #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] -struct Word { +pub struct Word { norm: String, native: Option, lemma: Option, @@ -48,7 +48,7 @@ struct Word { #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[diesel(table_name = wordrelation)] -struct WordRelation { +pub struct WordRelation { id: i32, wordsource: String, wordtarget: String, diff --git a/src/graphql.rs b/src/graphql.rs new file mode 100644 index 0000000..77f9e1d --- /dev/null +++ b/src/graphql.rs @@ -0,0 +1,63 @@ +use rocket::response::content::RawHtml; +use rocket::State; + +use juniper::EmptySubscription; +use juniper_rocket::{GraphQLRequest, GraphQLResponse}; + +use crate::db::models::languages::Language; +use crate::db::Database; + +#[derive(Debug)] +pub struct Query; + +#[juniper::graphql_object(Context = Database)] +impl Query { + #[graphql(name = "allLanguages")] + fn all_languages(context: &Database) -> Vec { + context.all_languages().unwrap() + } + + fn language(context: &Database, name: String) -> Option { + context.language(name.as_str()) + } +} + +pub struct Mutation; + +#[juniper::graphql_object(Context = Database)] +impl Mutation { + fn api_version() -> String { + "0.1".into() + } +} + +type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription>; + +pub fn create_schema() -> Schema { + Schema::new(Query {}, Mutation {}, EmptySubscription::default()) +} + +#[rocket::get("/")] +pub fn graphiql() -> RawHtml { + let graphql_endpoint_url = "/graphql"; + juniper_rocket::graphiql_source(graphql_endpoint_url, None) +} + +#[rocket::get("/graphql?")] +pub async fn get_graphql_handler( + context: &State, + request: GraphQLRequest, + schema: &State, +) -> GraphQLResponse { + request.execute(schema, context).await +} + +#[allow(clippy::needless_pass_by_value)] +#[rocket::post("/graphql", data = "")] +pub fn post_graphql_handler( + context: &State, + request: GraphQLRequest, + schema: &State +) -> GraphQLResponse { + request.execute_sync(schema, context) +} diff --git a/src/main.rs b/src/main.rs index 073d1f2..b4e774e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ #![warn(clippy::style, clippy::pedantic)] mod db; +mod graphql; use std::{env, error::Error}; @@ -44,6 +45,10 @@ fn make_cors() -> Result { #[rocket::main] async fn main() -> Result<(), Box> { + use graphql::{ + create_schema, get_graphql_handler, graphiql, post_graphql_handler, + }; + setup_logging(); info!("Reading environment variables"); @@ -52,7 +57,20 @@ async fn main() -> Result<(), Box> { let cors = make_cors()?; debug!("CORS: {:?}", cors); - #[allow(clippy::let_underscore_drop)] - let _ = rocket::build().attach(cors).launch().await?; + #[allow(clippy::let_underscore_drop, clippy::no_effect_underscore_binding)] + let _ = rocket::build() + .attach(cors) + .manage(db::Database::new()) + .manage(create_schema()) + .mount( + "/", + rocket::routes![ + graphiql, + get_graphql_handler, + post_graphql_handler + ], + ) + .launch() + .await?; Ok(()) }