First Commit

This commit is contained in:
Peter Schiwy 2024-05-24 14:25:46 +02:00
commit 116973d5ae
26 changed files with 1888 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1473
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

30
Cargo.toml Normal file
View File

@ -0,0 +1,30 @@
[package]
name = "axum-juniper"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1.37.0", features = [
"macros",
"net",
"rt-multi-thread",
"time",
] }
tokio-stream = "0.1.15"
r2d2 = { version = "0.8.10" }
juniper = { version = "0.16.1" }
axum = { version = "0.7.5", features = ["json", "macros", "tokio", "tracing"] }
juniper_axum = { version = "0.1.0", features = ["subscriptions"] }
diesel = { version = "2.1.6", features = ["postgres", "r2d2"] }
dotenvy = "0.15.7"
serde = "1.0.202"
serde_json = "1.0.117"
futures = "0.3.30"
anyhow = "1.0.86"
juniper_graphql_ws = { version = "0.4.0", features = ["graphql-transport-ws"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
[dev-dependencies]

0
README.md Normal file
View File

9
diesel.toml Normal file
View File

@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId"]
[migrations_directory]
dir = "migrations"

0
migrations/.keep Normal file
View File

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1 @@
DROP TABLE typen;-- This file should undo anything in `up.sql`

View File

@ -0,0 +1,4 @@
CREATE TABLE typen (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL
);

View File

@ -0,0 +1 @@
DROP TABLE hersteller;

View File

@ -0,0 +1,4 @@
CREATE TABLE hersteller (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL
);

View File

@ -0,0 +1 @@
DROP TABLE modelle;

View File

@ -0,0 +1,8 @@
CREATE TABLE modelle (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
typ_id SERIAL,
hersteller_id SERIAL,
CONSTRAINT fk_typ FOREIGN KEY(typ_id) REFERENCES typen(id),
CONSTRAINT fk_hersteller FOREIGN KEY (hersteller_id) REFERENCES hersteller(id)
)

39
src/database.rs Normal file
View File

@ -0,0 +1,39 @@
use diesel::r2d2::{ConnectionManager, Pool, PoolError};
use diesel::PgConnection;
use dotenvy::dotenv;
use std::env;
pub type PgPool = Pool<ConnectionManager<PgConnection>>;
fn init_pool(database_url: &str) -> Result<PgPool, PoolError> {
let manager = ConnectionManager::<PgConnection>::new(database_url);
Pool::builder().build(manager)
}
fn establish_connection() -> PgPool {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
init_pool(&database_url).unwrap_or_else(|_| panic!("Could not create database pool"))
}
#[derive(Clone, Debug)]
pub struct Database {
pub pool: PgPool,
}
impl Database {
pub fn new() -> Database {
Database {
pool: establish_connection(),
}
}
}
impl Default for Database {
fn default() -> Self {
Self::new()
}
}
impl juniper::Context for Database {}

1
src/diesel_dao/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod typ_dao;

28
src/diesel_dao/typ_dao.rs Normal file
View File

@ -0,0 +1,28 @@
use crate::{database::Database, models::typ::Typ};
use anyhow::{Ok, Result};
use diesel::{QueryDsl, RunQueryDsl, SelectableHelper};
pub struct TypDAO {}
impl TypDAO {
pub fn get_all_typen(context: &Database) -> Result<Vec<Typ>> {
use crate::schema::typen::dsl::*;
let connection = &mut context.pool.get()?;
let alle_typen = typen.load::<Typ>(connection)?;
Ok(alle_typen)
}
pub fn get_typ_by_id(context: &Database, typ_id: i32) -> Result<Typ> {
use crate::schema::typen::dsl::*;
let connection = &mut context.pool.get()?;
let typ = typen
.find(typ_id)
.select(Typ::as_select())
.first(connection)?;
Ok(typ)
}
}

74
src/main.rs Normal file
View File

@ -0,0 +1,74 @@
mod database;
mod diesel_dao;
mod models;
mod mutation;
mod query;
mod schema;
mod subscription;
use std::{net::SocketAddr, sync::Arc};
use axum::{
response::Html,
routing::{get, on, MethodFilter},
Extension, Router,
};
use juniper::RootNode;
use juniper_axum::{graphiql, graphql, playground, subscriptions};
use juniper_graphql_ws::ConnectionConfig;
use mutation::Mutation;
use query::Query;
use subscription::Subscription;
use tokio::net::TcpListener;
use crate::database::Database;
type Schema = RootNode<'static, Query, Mutation, Subscription>;
async fn homepage() -> Html<&'static str> {
"<html><h1>juniper_axum/simple example</h1>\
<div>visit <a href=\"/graphiql\">GraphiQL</a></div>\
<div>visit <a href=\"/playground\">GraphQL Playground</a></div>\
</html>"
.into()
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
let database = Database::new();
let schema = Schema::new(Query, Mutation, Subscription);
let app = Router::new()
.route(
"/graphql",
on(
MethodFilter::GET.or(MethodFilter::POST),
graphql::<Arc<Schema>>,
),
)
.route(
"/subscriptions",
get(subscriptions::ws::<Arc<Schema>>(ConnectionConfig::new(
database,
))),
)
.route("/graphiql", get(graphiql("/graphql", "/subscriptions")))
.route("/playground", get(playground("/graphql", "/subscriptions")))
.route("/", get(homepage))
.layer(Extension(Arc::new(schema)));
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
let listener = TcpListener::bind(addr)
.await
.unwrap_or_else(|e| panic!("failed to listen on {addr}: {e}"));
tracing::info!("listening on {addr}");
axum::serve(listener, app)
.await
.unwrap_or_else(|e| panic!("failed to run `axum::serve`: {e}"));
}

8
src/models/hersteller.rs Normal file
View File

@ -0,0 +1,8 @@
use diesel::Queryable;
#[derive(Queryable, Debug)]
#[diesel(table_name = hersteller)]
pub struct Hersteller {
pub id: i32,
pub name: String,
}

4
src/models/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod hersteller;
pub mod modell;
pub mod typ;

10
src/models/modell.rs Normal file
View File

@ -0,0 +1,10 @@
use diesel::Queryable;
#[derive(Queryable, Debug)]
#[diesel(table_name = modelle)]
pub struct Modell {
pub id: i32,
pub name: String,
pub typ_id: i32,
pub hersteller_id: i32,
}

23
src/models/typ.rs Normal file
View File

@ -0,0 +1,23 @@
use diesel::{associations::Identifiable, prelude::Insertable, Queryable, Selectable};
use juniper::{GraphQLInputObject, GraphQLObject};
use crate::schema::typen;
#[derive(GraphQLObject, Queryable, Selectable, Identifiable)]
#[diesel(table_name = typen)]
pub struct Typ {
pub id: i32,
pub name: String,
}
#[derive(GraphQLInputObject, Insertable)]
#[diesel(table_name = typen)]
pub struct TypInput {
pub name: String,
}
#[derive(GraphQLObject)]
pub struct TypResponse {
pub id: i32,
pub name: String,
}

47
src/mutation.rs Normal file
View File

@ -0,0 +1,47 @@
use crate::{
database::Database,
models::typ::{Typ, TypInput},
schema::typen,
};
use diesel::prelude::*;
use juniper::{graphql_object, FieldResult};
#[derive(Clone, Copy, Debug)]
pub struct Mutation;
#[graphql_object(context = Database)]
impl Mutation {
fn new_typ(#[graphql(context)] database: &Database, typ_input: TypInput) -> FieldResult<Typ> {
let connection = &mut database.pool.get()?;
let typ = diesel::insert_into(typen::table)
.values(&typ_input)
.get_result::<Typ>(connection)?;
Ok(typ)
}
fn edit_typ(
#[graphql(context)] database: &Database,
typ_id: i32,
typ_name: String,
) -> FieldResult<Typ> {
use crate::schema::typen::dsl::*;
let connection = &mut database.pool.get()?;
let update_typ = diesel::update(typen.find(typ_id))
.set(name.eq(typ_name))
.get_result::<Typ>(connection)?;
Ok(update_typ)
}
fn delete_typ(#[graphql(context)] database: &Database, typ_id: i32) -> FieldResult<String> {
use crate::schema::typen::dsl::*;
let conn = &mut database.pool.get()?;
diesel::delete(typen.find(typ_id)).execute(conn)?;
Ok("Typ wurde erfolgreich gelöscht".to_string())
}
}

22
src/query.rs Normal file
View File

@ -0,0 +1,22 @@
use crate::{database::Database, diesel_dao::typ_dao::TypDAO, models::typ::Typ};
use juniper::{graphql_object, FieldResult};
pub struct Query;
#[graphql_object(context = Database)]
impl Query {
/// Zeigt alle Gerätetypen an.
fn typen(#[graphql(context)] context: &Database) -> FieldResult<Vec<Typ>> {
let alle_typen = TypDAO::get_all_typen(context)?;
Ok(alle_typen)
}
/// Zeigt einen Gerätetypen an
fn typ(
#[graphql(context)] context: &Database,
#[graphql(description = "Die ID des Typs.")] typ_id: i32,
) -> FieldResult<Typ> {
let typ = TypDAO::get_typ_by_id(context, typ_id)?;
Ok(typ)
}
}

33
src/schema.rs Normal file
View File

@ -0,0 +1,33 @@
// @generated automatically by Diesel CLI.
diesel::table! {
hersteller (id) {
id -> Int4,
name -> Varchar,
}
}
diesel::table! {
modelle (id) {
id -> Int4,
name -> Varchar,
typ_id -> Int4,
hersteller_id -> Int4,
}
}
diesel::table! {
typen (id) {
id -> Int4,
name -> Varchar,
}
}
diesel::joinable!(modelle -> hersteller (hersteller_id));
diesel::joinable!(modelle -> typen (typ_id));
diesel::allow_tables_to_appear_in_same_query!(
hersteller,
modelle,
typen,
);

25
src/subscription.rs Normal file
View File

@ -0,0 +1,25 @@
use futures::stream::{BoxStream, StreamExt as _};
use juniper::{graphql_subscription, FieldError};
use std::time::Duration;
use tokio::time::interval;
use tokio_stream::wrappers::IntervalStream;
use crate::database::Database;
#[derive(Clone, Copy, Debug)]
pub struct Subscription;
type NumberStream = BoxStream<'static, Result<i32, FieldError>>;
#[graphql_subscription(context = Database)]
impl Subscription {
/// Counts seconds.
async fn count() -> NumberStream {
let mut value = 0;
let stream = IntervalStream::new(interval(Duration::from_secs(1))).map(move |_| {
value += 1;
Ok(value)
});
Box::pin(stream)
}
}