Initial GraphQL API sort of working

This commit is contained in:
Lucien Cartier-Tilet 2023-01-04 19:31:52 +01:00
parent a2a2863d62
commit 8d5e523ab3
Signed by: phundrak
GPG Key ID: BD7789E705CB8DCA
7 changed files with 206 additions and 13 deletions

View File

@ -2,6 +2,12 @@
name = "ordabok" name = "ordabok"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
license = "AGPL-3.0"
authors = ["Lucien Cartier-Tilet <lucien@phundrak.com>"]
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 # 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" dotenvy = "0.15"
# Database # 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"] } diesel-derive-enum = { version = "2.0.0-rc.0", features = ["postgres"] }
chrono = "0.4.23" chrono = "0.4.23"
@ -18,6 +24,10 @@ chrono = "0.4.23"
rocket = "0.5.0-rc.2" rocket = "0.5.0-rc.2"
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e814" } rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e814" }
# GraphQL
juniper = "0.15.10"
juniper_rocket = "0.8.2"
# logging # logging
tracing = "0.1.37" tracing = "0.1.37"
tracing-subscriber = "0.3.16" tracing-subscriber = "0.3.16"

View File

@ -1,2 +1,73 @@
pub mod models; pub mod models;
pub mod schema; 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<ConnectionManager<PgConnection>>,
}
impl juniper::Context for Database {}
impl Database {
pub fn new() -> Self {
Self {
conn: Database::get_connection_pool(),
}
}
pub fn get_connection_pool() -> Pool<ConnectionManager<PgConnection>> {
dotenv().ok();
let database_url =
env::var("DATABASE_URL").expect("DATABASE_URL must be set!");
info!("Connecting to {}", database_url);
let manager = ConnectionManager::<PgConnection>::new(database_url);
Pool::builder()
.test_on_check_out(true)
.build(manager)
.expect("Could not build connection pool")
}
fn conn(
&self,
) -> Result<PooledConnection<ConnectionManager<PgConnection>>, ()> {
self.conn
.get()
.map_err(|e| info!("Failed to connect to database: {:?}", e))
}
pub fn all_languages(&self) -> Result<Vec<Language>, ()> {
use self::schema::languages::dsl::languages;
languages.load::<Language>(&mut self.conn()?).map_err(|e| {
info!("Failed to retrieve languages from database: {:?}", e);
})
}
pub fn language(&self, name: &str) -> Option<Language> {
use self::schema::languages::dsl::languages;
match &mut self.conn() {
Ok(val) => languages
.find(name.to_string())
.first::<Language>(val)
.map_or_else(
|e| {
info!(
"Failed to retrieve language {} from database: {:?}",
name, e
);
None
},
Some,
),
Err(_) => None,
}
}
}

View File

@ -1,16 +1,18 @@
use super::super::schema::{langandagents, languages}; use super::super::schema::{langandagents, languages};
use diesel::prelude::*; 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"] #[DieselTypePath = "crate::db::schema::sql_types::Release"]
pub enum Release { pub enum Release {
Public, Public,
#[graphql(name="NON_COMMERCIAL")]
NonCommercial, NonCommercial,
Research, Research,
Private, 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"] #[DieselTypePath = "crate::db::schema::sql_types::Dictgenre"]
pub enum DictGenre { pub enum DictGenre {
General, General,
@ -22,7 +24,7 @@ pub enum DictGenre {
Terminology, 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"] #[DieselTypePath = "crate::db::schema::sql_types::Agentlanguagerelation"]
pub enum AgentLanguageRelation { pub enum AgentLanguageRelation {
Publisher, Publisher,
@ -31,17 +33,35 @@ pub enum AgentLanguageRelation {
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
pub struct Language { pub struct Language {
release: Release,
created: chrono::NaiveDateTime,
name: String, name: String,
owner: String,
targetlanguage: Vec<String>,
genre: Vec<DictGenre>,
native: Option<String>, native: Option<String>,
release: Release,
targetlanguage: Vec<Option<String>>,
genre: Vec<Option<DictGenre>>,
abstract_: Option<String>, abstract_: Option<String>,
created: chrono::NaiveDateTime,
description: Option<String>, description: Option<String>,
rights: Option<String>, rights: Option<String>,
license: Option<String>, license: Option<String>,
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)] #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]

View File

@ -7,6 +7,17 @@ pub struct User {
pub username: String, 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)] #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
#[diesel(table_name = userfollows)] #[diesel(table_name = userfollows)]
pub struct UserFollow { pub struct UserFollow {

View File

@ -31,7 +31,7 @@ pub enum PartOfSpeech {
} }
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
struct Word { pub struct Word {
norm: String, norm: String,
native: Option<String>, native: Option<String>,
lemma: Option<String>, lemma: Option<String>,
@ -48,7 +48,7 @@ struct Word {
#[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)] #[derive(Queryable, Insertable, Debug, Clone, PartialEq, Eq)]
#[diesel(table_name = wordrelation)] #[diesel(table_name = wordrelation)]
struct WordRelation { pub struct WordRelation {
id: i32, id: i32,
wordsource: String, wordsource: String,
wordtarget: String, wordtarget: String,

63
src/graphql.rs Normal file
View File

@ -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<Language> {
context.all_languages().unwrap()
}
fn language(context: &Database, name: String) -> Option<Language> {
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<Database>>;
pub fn create_schema() -> Schema {
Schema::new(Query {}, Mutation {}, EmptySubscription::default())
}
#[rocket::get("/")]
pub fn graphiql() -> RawHtml<String> {
let graphql_endpoint_url = "/graphql";
juniper_rocket::graphiql_source(graphql_endpoint_url, None)
}
#[rocket::get("/graphql?<request>")]
pub async fn get_graphql_handler(
context: &State<Database>,
request: GraphQLRequest,
schema: &State<Schema>,
) -> GraphQLResponse {
request.execute(schema, context).await
}
#[allow(clippy::needless_pass_by_value)]
#[rocket::post("/graphql", data = "<request>")]
pub fn post_graphql_handler(
context: &State<Database>,
request: GraphQLRequest,
schema: &State<Schema>
) -> GraphQLResponse {
request.execute_sync(schema, context)
}

View File

@ -1,6 +1,7 @@
#![warn(clippy::style, clippy::pedantic)] #![warn(clippy::style, clippy::pedantic)]
mod db; mod db;
mod graphql;
use std::{env, error::Error}; use std::{env, error::Error};
@ -44,6 +45,10 @@ fn make_cors() -> Result<rocket_cors::Cors, rocket_cors::Error> {
#[rocket::main] #[rocket::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
use graphql::{
create_schema, get_graphql_handler, graphiql, post_graphql_handler,
};
setup_logging(); setup_logging();
info!("Reading environment variables"); info!("Reading environment variables");
@ -52,7 +57,20 @@ async fn main() -> Result<(), Box<dyn Error>> {
let cors = make_cors()?; let cors = make_cors()?;
debug!("CORS: {:?}", cors); debug!("CORS: {:?}", cors);
#[allow(clippy::let_underscore_drop)] #[allow(clippy::let_underscore_drop, clippy::no_effect_underscore_binding)]
let _ = rocket::build().attach(cors).launch().await?; 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(()) Ok(())
} }