diff --git a/Cargo.lock b/Cargo.lock index 60fe4b3..5a4c127 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,6 +317,7 @@ dependencies = [ "dotenv", "env_logger", "envy", + "itertools 0.13.0", "log", "serde", "sqlx", @@ -1138,6 +1139,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -1986,7 +1996,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" dependencies = [ - "itertools", + "itertools 0.12.1", "nom", "unicode_categories", ] @@ -2647,6 +2657,18 @@ checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 447723f..419301c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,12 @@ serde = "1.0.215" tokio = { version = "1.39.1", features = ["macros", "rt-multi-thread"] } # UUID -uuid = { version = "1.11.0", features = ["serde", "v4"] } +uuid = { version = "1.11.0", features = ["serde", "v4", "macro-diagnostics"] } ulid = { version = "1.1.3", features = ["uuid"] } # Logging tracing = "0.1.41" tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] } tracing-appender = "0.2.3" + +itertools = "0.13.0" diff --git a/migrations/20241202221730_liegenschften.down.sql b/migrations/20241202221730_liegenschften.down.sql new file mode 100644 index 0000000..8367fba --- /dev/null +++ b/migrations/20241202221730_liegenschften.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +DROP TYPE gruppen_herkunft;; diff --git a/migrations/20241202221730_liegenschften.up.sql b/migrations/20241202221730_liegenschften.up.sql new file mode 100644 index 0000000..c8850aa --- /dev/null +++ b/migrations/20241202221730_liegenschften.up.sql @@ -0,0 +1,5 @@ +CREATE TYPE gruppen_herkunft AS ENUM ( + 'direkt', + 'indirekt', + 'beides' +); diff --git a/src/dataloader/benutzer_gruppen.rs b/src/dataloader/benutzer_gruppen.rs new file mode 100644 index 0000000..e16a418 --- /dev/null +++ b/src/dataloader/benutzer_gruppen.rs @@ -0,0 +1,51 @@ +use async_graphql::dataloader::*; +use async_graphql::*; +use itertools::Itertools; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::models::gruppe::Gruppe; +use crate::scalar::Id; + +pub struct BenutzerGruppenLoader { + pub pool: sqlx::PgPool, +} + +impl Loader for BenutzerGruppenLoader { + type Value = Vec; + type Error = Arc; + + async fn load(&self, keys: &[Id]) -> Result, Self::Error> { + let rows = sqlx::query!( + r#" + SELECT + bg.benutzer_id, + g.id, + g.gruppenname, + g.erstellt_am, + g.geaendert_am + FROM gruppen AS g + LEFT JOIN benutzer_gruppen AS bg ON g.id = bg.gruppe_id + WHERE bg.benutzer_id = ANY($1); + "#, + keys + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|row| { + ( + row.benutzer_id, + Gruppe { + id: row.id, + gruppenname: row.gruppenname, + erstellt_am: row.erstellt_am, + geaendert_am: row.geaendert_am, + }, + ) + }) + .into_group_map(); + + Ok(rows) + } +} diff --git a/src/dataloader/benutzer_gruppen_kumulativ.rs b/src/dataloader/benutzer_gruppen_kumulativ.rs new file mode 100644 index 0000000..af4c56d --- /dev/null +++ b/src/dataloader/benutzer_gruppen_kumulativ.rs @@ -0,0 +1,72 @@ +use async_graphql::dataloader::*; +use async_graphql::*; +use itertools::Itertools; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::models::gruppe_ansicht::{GruppeAnsicht, Herkunft}; +use crate::scalar::Id; + +pub struct BenutzerGruppenKumulativLoader { + pub pool: sqlx::PgPool, +} + +impl Loader for BenutzerGruppenKumulativLoader { + type Value = Vec; + type Error = Arc; + + async fn load(&self, keys: &[Id]) -> Result, Self::Error> { + let rows = sqlx::query!( + r#" + SELECT DISTINCT + COALESCE(bg.benutzer_id, br.benutzer_id) AS benutzer_id, + g.id AS gruppe_id, + g.gruppenname, + g.erstellt_am, + g.geaendert_am, + CASE WHEN br.benutzer_id IS NOT null THEN r.rollenname END AS rollenname, + CASE + WHEN bg.benutzer_id IS NOT NULL AND br.benutzer_id IS NOT NULL THEN 'beides'::gruppen_herkunft + WHEN bg.benutzer_id IS NOT NULL THEN 'direkt'::gruppen_herkunft + WHEN br.benutzer_id IS NOT NULL THEN 'indirekt'::gruppen_herkunft + END AS "herkunft: Herkunft" + FROM gruppen AS g + LEFT JOIN + benutzer_gruppen AS bg + ON + g.id = bg.gruppe_id + AND bg.benutzer_id = ANY($1) + LEFT JOIN rollen_gruppen AS rg ON g.id = rg.gruppe_id + LEFT JOIN + benutzer_rollen AS br + ON + rg.rolle_id = br.rolle_id + AND br.benutzer_id = ANY($1) + LEFT JOIN rollen AS r ON rg.rolle_id = r.id + WHERE + bg.benutzer_id = ANY($1) + OR br.benutzer_id = ANY($1); + "#, + keys + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|row| { + ( + row.benutzer_id.unwrap(), + GruppeAnsicht { + id: row.gruppe_id, + gruppenname: row.gruppenname, + rollenname: row.rollenname, + herkunft: row.herkunft.unwrap(), + erstellt_am: row.erstellt_am, + geaendert_am: row.geaendert_am, + }, + ) + }) + .into_group_map(); + + Ok(rows) + } +} diff --git a/src/dataloader/benutzer_rollen.rs b/src/dataloader/benutzer_rollen.rs new file mode 100644 index 0000000..a093e0d --- /dev/null +++ b/src/dataloader/benutzer_rollen.rs @@ -0,0 +1,51 @@ +use async_graphql::dataloader::*; +use async_graphql::*; +use itertools::Itertools; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::models::rolle::Rolle; +use crate::scalar::Id; + +pub struct BenutzerRollenLoader { + pub pool: sqlx::PgPool, +} + +impl Loader for BenutzerRollenLoader { + type Value = Vec; + type Error = Arc; + + async fn load(&self, keys: &[Id]) -> Result, Self::Error> { + let rows = sqlx::query!( + r#" + SELECT + br.benutzer_id, + r.id, + r.rollenname, + r.erstellt_am, + r.geaendert_am + FROM rollen AS r + LEFT JOIN benutzer_rollen AS br ON r.id = br.rolle_id + WHERE br.benutzer_id = ANY($1); + "#, + keys + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|row| { + ( + row.benutzer_id, + Rolle { + id: row.id, + rollenname: row.rollenname, + erstellt_am: row.erstellt_am, + geaendert_am: row.geaendert_am, + }, + ) + }) + .into_group_map(); + + Ok(rows) + } +} diff --git a/src/dataloader/mod.rs b/src/dataloader/mod.rs new file mode 100644 index 0000000..b28fc43 --- /dev/null +++ b/src/dataloader/mod.rs @@ -0,0 +1,41 @@ +mod benutzer_gruppen; +mod benutzer_gruppen_kumulativ; +mod benutzer_rollen; +mod rollen_gruppen; + +pub use benutzer_gruppen::BenutzerGruppenLoader; +pub use benutzer_gruppen_kumulativ::BenutzerGruppenKumulativLoader; +pub use benutzer_rollen::BenutzerRollenLoader; +pub use rollen_gruppen::RollenGruppenLoader; + +use async_graphql::dataloader::DataLoader; + +pub struct LoaderContext { + pub benutzer_gruppen: DataLoader, + pub benutzer_rollen: DataLoader, + pub rollen_gruppen: DataLoader, + pub benutzer_gruppen_kumulativ: DataLoader, +} + +impl LoaderContext { + pub fn new(pool: sqlx::PgPool) -> Self { + Self { + benutzer_gruppen: DataLoader::new( + BenutzerGruppenLoader { pool: pool.clone() }, + tokio::spawn, + ), + benutzer_rollen: DataLoader::new( + BenutzerRollenLoader { pool: pool.clone() }, + tokio::spawn, + ), + rollen_gruppen: DataLoader::new( + RollenGruppenLoader { pool: pool.clone() }, + tokio::spawn, + ), + benutzer_gruppen_kumulativ: DataLoader::new( + BenutzerGruppenKumulativLoader { pool: pool.clone() }, + tokio::spawn, + ), + } + } +} diff --git a/src/dataloader/rollen_gruppen.rs b/src/dataloader/rollen_gruppen.rs new file mode 100644 index 0000000..21ddeee --- /dev/null +++ b/src/dataloader/rollen_gruppen.rs @@ -0,0 +1,51 @@ +use async_graphql::dataloader::*; +use async_graphql::*; +use itertools::Itertools; +use std::collections::HashMap; +use std::sync::Arc; + +use crate::models::gruppe::Gruppe; +use crate::scalar::Id; + +pub struct RollenGruppenLoader { + pub pool: sqlx::PgPool, +} + +impl Loader for RollenGruppenLoader { + type Value = Vec; + type Error = Arc; + + async fn load(&self, keys: &[Id]) -> Result, Self::Error> { + let rows = sqlx::query!( + r#" + SELECT + rg.rolle_id, + g.id, + g.gruppenname, + g.erstellt_am, + g.geaendert_am + FROM gruppen AS g + LEFT JOIN rollen_gruppen AS rg ON g.id = rg.gruppe_id + WHERE rg.rolle_id = ANY($1); + "#, + keys + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|row| { + ( + row.rolle_id, + Gruppe { + id: row.id, + gruppenname: row.gruppenname, + erstellt_am: row.erstellt_am, + geaendert_am: row.geaendert_am, + }, + ) + }) + .into_group_map(); + + Ok(rows) + } +} diff --git a/src/format.sql b/src/format.sql index 3c0c3c5..113b93a 100644 --- a/src/format.sql +++ b/src/format.sql @@ -1,8 +1,24 @@ -SELECT - g.id, +SELECT DISTINCT + COALESCE(bg.benutzer_id, br.benutzer_id) AS benutzer_id, + g.id AS gruppe_id, g.gruppenname, g.erstellt_am, - g.geaendert_am + g.geaendert_am, + CASE WHEN br.benutzer_id IS NOT null THEN r.rollenname END + AS rollenname FROM gruppen AS g +LEFT JOIN + benutzer_gruppen AS bg + ON + g.id = bg.gruppe_id + AND bg.benutzer_id = ANY($1) LEFT JOIN rollen_gruppen AS rg ON g.id = rg.gruppe_id -WHERE rg.rolle_id = 1; +LEFT JOIN + benutzer_rollen AS br + ON + rg.rolle_id = br.rolle_id + AND br.benutzer_id = ANY($1) +LEFT JOIN rollen AS r ON rg.rolle_id = r.id +WHERE + bg.benutzer_id = ANY($1) + OR br.benutzer_id = ANY($1); diff --git a/src/main.rs b/src/main.rs index 4a33469..0bf179b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use axum::{ Router, }; use config::Config; +use dataloader::LoaderContext; use dotenv::dotenv; use mutations::Mutation; use queries::Query; @@ -18,6 +19,7 @@ use tokio::net::TcpListener; mod config; mod database; +mod dataloader; mod domain; mod models; mod mutations; @@ -38,12 +40,11 @@ async fn graphiql() -> impl IntoResponse { #[tokio::main] async fn main() -> Result<()> { - // tracing_subscriber::fmt() - // .with_max_level(tracing::Level::DEBUG) - // .init(); + tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .init(); dotenv().ok(); - env_logger::init(); // Config::load(); let config = Arc::new(Config::load()?); @@ -51,7 +52,8 @@ async fn main() -> Result<()> { let db_pool = PgPool::connect(&config.database.url).await?; let schema = Schema::build(Query::default(), Mutation::default(), EmptySubscription) - .data(db_pool) + .data(LoaderContext::new(db_pool.clone())) + .data(db_pool.clone()) .finish(); // println!("{}", &schema.sdl()); diff --git a/src/models/benutzer.rs b/src/models/benutzer.rs index c4759d1..cc2066a 100644 --- a/src/models/benutzer.rs +++ b/src/models/benutzer.rs @@ -1,11 +1,10 @@ use async_graphql::{ComplexObject, Context, FieldResult, SimpleObject}; -use sqlx::PgPool; -use crate::{models::gruppe::Gruppe, scalar::Id}; +use crate::{dataloader::LoaderContext, models::gruppe::Gruppe, models::rolle::Rolle, scalar::Id}; -use super::rolle::Rolle; +use super::gruppe_ansicht::GruppeAnsicht; -#[derive(SimpleObject)] +#[derive(sqlx::FromRow, SimpleObject)] #[graphql(complex)] pub struct Benutzer { /// Die UUID eines Benutzers @@ -24,50 +23,22 @@ pub struct Benutzer { #[ComplexObject] impl Benutzer { /// Die Rollen des Benutzers - pub async fn rollen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult> { - let pool = ctx.data::()?; - - let rows = sqlx::query_as!( - Rolle, - r#" - SELECT - r.id, - r.rollenname, - r.erstellt_am, - r.geaendert_am - FROM rollen AS r - LEFT JOIN benutzer_rollen AS br ON r.id = br.rolle_id - WHERE br.benutzer_id = $1; - "#, - &self.id - ) - .fetch_all(pool) - .await?; - - Ok(rows) + pub async fn rollen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult>> { + let loader = ctx.data::()?; + Ok(loader.benutzer_rollen.load_one(self.id).await?) } /// Die Gruppen des Benutzers - pub async fn gruppen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult> { - let pool = ctx.data::()?; - - let rows = sqlx::query_as!( - Gruppe, - r#" - SELECT - g.id, - g.gruppenname, - g.erstellt_am, - g.geaendert_am - FROM gruppen AS g - LEFT JOIN benutzer_gruppen AS bg ON g.id = bg.gruppe_id - WHERE bg.benutzer_id = $1; - "#, - &self.id - ) - .fetch_all(pool) - .await?; - - Ok(rows) + pub async fn gruppen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult>> { + let loader = ctx.data::()?; + Ok(loader.benutzer_gruppen.load_one(self.id).await?) + } + /// Die Gruppen eines Benutzer kumulativ mit den Gruppen aus den Rollen + pub async fn gruppen_kumulativ<'ctx>( + &self, + ctx: &Context<'ctx>, + ) -> FieldResult>> { + let loader = ctx.data::()?; + Ok(loader.benutzer_gruppen_kumulativ.load_one(self.id).await?) } } diff --git a/src/models/gruppe.rs b/src/models/gruppe.rs index c52c50a..d350d38 100644 --- a/src/models/gruppe.rs +++ b/src/models/gruppe.rs @@ -2,7 +2,7 @@ use async_graphql::{ComplexObject, SimpleObject}; use crate::scalar::{Id, Time}; -#[derive(SimpleObject)] +#[derive(sqlx::FromRow, SimpleObject, Clone, Debug)] #[graphql(complex)] pub struct Gruppe { /// Die UUIDl einer Gruppe diff --git a/src/models/gruppe_ansicht.rs b/src/models/gruppe_ansicht.rs new file mode 100644 index 0000000..1e2006f --- /dev/null +++ b/src/models/gruppe_ansicht.rs @@ -0,0 +1,43 @@ +use async_graphql::{ComplexObject, Enum, SimpleObject}; +use sqlx::Type; + +use crate::scalar::{Id, Time}; + +/// Aus welcher Quille die Gruppe stammt. +#[derive(Enum, Clone, Debug, Eq, Copy, PartialEq, Type)] +#[sqlx(type_name = "gruppen_herkunft", rename_all = "lowercase")] +pub enum Herkunft { + /// Die Gruppe wurde direkt dem Benutzer zugefügt + Direkt, + + /// Die Gruppe wurde durch eine Rolle vergeben + Indirekt, + + /// Die Gruppe ist sowohl direkt als auch indirekt zugefügt + Beides, +} + +#[derive(sqlx::FromRow, SimpleObject, Clone, Debug)] +#[graphql(complex)] +pub struct GruppeAnsicht { + /// Die UUID einer Gruppe + pub id: Id, + + /// Der Gruppenname + pub gruppenname: String, + + /// Die Herkunft der Gruppe + pub herkunft: Herkunft, + + /// Bei einer indirekten Zuweisung, wird der Rollenname angegeben + pub rollenname: Option, + + /// Wann die Gruppe erstellt wurde + pub erstellt_am: Time, + + /// Wann die Gruppe geaendert wurde + pub geaendert_am: Time, +} + +#[ComplexObject] +impl GruppeAnsicht {} diff --git a/src/models/mod.rs b/src/models/mod.rs index c4375b6..db5c545 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,5 +1,6 @@ pub mod benutzer; pub mod gruppe; +pub mod gruppe_ansicht; pub mod hersteller; pub mod modell; pub mod rolle; diff --git a/src/models/rolle.rs b/src/models/rolle.rs index be33861..9af0f3a 100644 --- a/src/models/rolle.rs +++ b/src/models/rolle.rs @@ -1,12 +1,13 @@ use async_graphql::{ComplexObject, Context, FieldResult, SimpleObject}; -use sqlx::PgPool; -use crate::scalar::{Id, Time}; - -use super::gruppe::Gruppe; +use crate::{ + dataloader::LoaderContext, + models::gruppe::Gruppe, + scalar::{Id, Time}, +}; /// Um die Administration zu erleichtern werden Gruppen in die Rollen hinzugefuegt -#[derive(SimpleObject, Debug)] +#[derive(sqlx::FromRow, SimpleObject, Debug, Clone)] #[graphql(complex)] pub struct Rolle { /// Die uuid einer Rolle @@ -25,26 +26,29 @@ pub struct Rolle { #[ComplexObject] impl Rolle { /// Die Gruppen in einer Rolle - pub async fn gruppen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult> { - let pool = ctx.data::()?; - - let rows = sqlx::query_as!( - Gruppe, - r#" - SELECT - g.id, - g.gruppenname, - g.erstellt_am, - g.geaendert_am - FROM gruppen AS g - LEFT JOIN rollen_gruppen AS rg ON g.id = rg.gruppe_id - WHERE rg.rolle_id = $1; - "#, - &self.id - ) - .fetch_all(pool) - .await?; - - Ok(rows) + pub async fn gruppen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult>> { + let loader = ctx.data::()?; + Ok(loader.rollen_gruppen.load_one(self.id).await?) + // + // let pool = ctx.data::()?; + // + // let rows = sqlx::query_as!( + // Gruppe, + // r#" + // SELECT + // g.id, + // g.gruppenname, + // g.erstellt_am, + // g.geaendert_am + // FROM gruppen AS g + // LEFT JOIN rollen_gruppen AS rg ON g.id = rg.gruppe_id + // WHERE rg.rolle_id = $1; + // "#, + // &self.id + // ) + // .fetch_all(pool) + // .await?; + // + // Ok(rows) } } diff --git a/src/mutations/mod.rs b/src/mutations/mod.rs index bf1760c..dc35495 100644 --- a/src/mutations/mod.rs +++ b/src/mutations/mod.rs @@ -1,6 +1,7 @@ pub mod hersteller; pub mod modell; pub mod typ; + use async_graphql::MergedObject; #[derive(MergedObject, Default)]