Dataloader für BenutzerGruppen und BenutzerRollen hinzugefügt

This commit is contained in:
Peter Schiwy 2024-12-02 23:44:47 +01:00
parent f410791d4b
commit bfce29c8ee
17 changed files with 419 additions and 84 deletions

24
Cargo.lock generated
View File

@ -317,6 +317,7 @@ dependencies = [
"dotenv", "dotenv",
"env_logger", "env_logger",
"envy", "envy",
"itertools 0.13.0",
"log", "log",
"serde", "serde",
"sqlx", "sqlx",
@ -1138,6 +1139,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.11"
@ -1986,7 +1996,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
dependencies = [ dependencies = [
"itertools", "itertools 0.12.1",
"nom", "nom",
"unicode_categories", "unicode_categories",
] ]
@ -2647,6 +2657,18 @@ checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"serde", "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]] [[package]]

View File

@ -31,10 +31,12 @@ serde = "1.0.215"
tokio = { version = "1.39.1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.39.1", features = ["macros", "rt-multi-thread"] }
# UUID # 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"] } ulid = { version = "1.1.3", features = ["uuid"] }
# Logging # Logging
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter", "chrono"] }
tracing-appender = "0.2.3" tracing-appender = "0.2.3"
itertools = "0.13.0"

View File

@ -0,0 +1,2 @@
-- Add down migration script here
DROP TYPE gruppen_herkunft;;

View File

@ -0,0 +1,5 @@
CREATE TYPE gruppen_herkunft AS ENUM (
'direkt',
'indirekt',
'beides'
);

View File

@ -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<Id> for BenutzerGruppenLoader {
type Value = Vec<Gruppe>;
type Error = Arc<sqlx::Error>;
async fn load(&self, keys: &[Id]) -> Result<HashMap<Id, Self::Value>, 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)
}
}

View File

@ -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<Id> for BenutzerGruppenKumulativLoader {
type Value = Vec<GruppeAnsicht>;
type Error = Arc<sqlx::Error>;
async fn load(&self, keys: &[Id]) -> Result<HashMap<Id, Self::Value>, 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)
}
}

View File

@ -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<Id> for BenutzerRollenLoader {
type Value = Vec<Rolle>;
type Error = Arc<sqlx::Error>;
async fn load(&self, keys: &[Id]) -> Result<HashMap<Id, Self::Value>, 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)
}
}

41
src/dataloader/mod.rs Normal file
View File

@ -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<BenutzerGruppenLoader>,
pub benutzer_rollen: DataLoader<BenutzerRollenLoader>,
pub rollen_gruppen: DataLoader<RollenGruppenLoader>,
pub benutzer_gruppen_kumulativ: DataLoader<BenutzerGruppenKumulativLoader>,
}
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,
),
}
}
}

View File

@ -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<Id> for RollenGruppenLoader {
type Value = Vec<Gruppe>;
type Error = Arc<sqlx::Error>;
async fn load(&self, keys: &[Id]) -> Result<HashMap<Id, Self::Value>, 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)
}
}

View File

@ -1,8 +1,24 @@
SELECT SELECT DISTINCT
g.id, COALESCE(bg.benutzer_id, br.benutzer_id) AS benutzer_id,
g.id AS gruppe_id,
g.gruppenname, g.gruppenname,
g.erstellt_am, 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 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 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);

View File

@ -9,6 +9,7 @@ use axum::{
Router, Router,
}; };
use config::Config; use config::Config;
use dataloader::LoaderContext;
use dotenv::dotenv; use dotenv::dotenv;
use mutations::Mutation; use mutations::Mutation;
use queries::Query; use queries::Query;
@ -18,6 +19,7 @@ use tokio::net::TcpListener;
mod config; mod config;
mod database; mod database;
mod dataloader;
mod domain; mod domain;
mod models; mod models;
mod mutations; mod mutations;
@ -38,12 +40,11 @@ async fn graphiql() -> impl IntoResponse {
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
// tracing_subscriber::fmt() tracing_subscriber::fmt()
// .with_max_level(tracing::Level::DEBUG) .with_max_level(tracing::Level::TRACE)
// .init(); .init();
dotenv().ok(); dotenv().ok();
env_logger::init();
// Config::load(); // Config::load();
let config = Arc::new(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 db_pool = PgPool::connect(&config.database.url).await?;
let schema = Schema::build(Query::default(), Mutation::default(), EmptySubscription) let schema = Schema::build(Query::default(), Mutation::default(), EmptySubscription)
.data(db_pool) .data(LoaderContext::new(db_pool.clone()))
.data(db_pool.clone())
.finish(); .finish();
// println!("{}", &schema.sdl()); // println!("{}", &schema.sdl());

View File

@ -1,11 +1,10 @@
use async_graphql::{ComplexObject, Context, FieldResult, SimpleObject}; 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)] #[graphql(complex)]
pub struct Benutzer { pub struct Benutzer {
/// Die UUID eines Benutzers /// Die UUID eines Benutzers
@ -24,50 +23,22 @@ pub struct Benutzer {
#[ComplexObject] #[ComplexObject]
impl Benutzer { impl Benutzer {
/// Die Rollen des Benutzers /// Die Rollen des Benutzers
pub async fn rollen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult<Vec<Rolle>> { pub async fn rollen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult<Option<Vec<Rolle>>> {
let pool = ctx.data::<PgPool>()?; let loader = ctx.data::<LoaderContext>()?;
Ok(loader.benutzer_rollen.load_one(self.id).await?)
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)
} }
/// Die Gruppen des Benutzers /// Die Gruppen des Benutzers
pub async fn gruppen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult<Vec<Gruppe>> { pub async fn gruppen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult<Option<Vec<Gruppe>>> {
let pool = ctx.data::<PgPool>()?; let loader = ctx.data::<LoaderContext>()?;
Ok(loader.benutzer_gruppen.load_one(self.id).await?)
let rows = sqlx::query_as!( }
Gruppe, /// Die Gruppen eines Benutzer kumulativ mit den Gruppen aus den Rollen
r#" pub async fn gruppen_kumulativ<'ctx>(
SELECT &self,
g.id, ctx: &Context<'ctx>,
g.gruppenname, ) -> FieldResult<Option<Vec<GruppeAnsicht>>> {
g.erstellt_am, let loader = ctx.data::<LoaderContext>()?;
g.geaendert_am Ok(loader.benutzer_gruppen_kumulativ.load_one(self.id).await?)
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)
} }
} }

View File

@ -2,7 +2,7 @@ use async_graphql::{ComplexObject, SimpleObject};
use crate::scalar::{Id, Time}; use crate::scalar::{Id, Time};
#[derive(SimpleObject)] #[derive(sqlx::FromRow, SimpleObject, Clone, Debug)]
#[graphql(complex)] #[graphql(complex)]
pub struct Gruppe { pub struct Gruppe {
/// Die UUIDl einer Gruppe /// Die UUIDl einer Gruppe

View File

@ -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<String>,
/// Wann die Gruppe erstellt wurde
pub erstellt_am: Time,
/// Wann die Gruppe geaendert wurde
pub geaendert_am: Time,
}
#[ComplexObject]
impl GruppeAnsicht {}

View File

@ -1,5 +1,6 @@
pub mod benutzer; pub mod benutzer;
pub mod gruppe; pub mod gruppe;
pub mod gruppe_ansicht;
pub mod hersteller; pub mod hersteller;
pub mod modell; pub mod modell;
pub mod rolle; pub mod rolle;

View File

@ -1,12 +1,13 @@
use async_graphql::{ComplexObject, Context, FieldResult, SimpleObject}; use async_graphql::{ComplexObject, Context, FieldResult, SimpleObject};
use sqlx::PgPool;
use crate::scalar::{Id, Time}; use crate::{
dataloader::LoaderContext,
use super::gruppe::Gruppe; models::gruppe::Gruppe,
scalar::{Id, Time},
};
/// Um die Administration zu erleichtern werden Gruppen in die Rollen hinzugefuegt /// Um die Administration zu erleichtern werden Gruppen in die Rollen hinzugefuegt
#[derive(SimpleObject, Debug)] #[derive(sqlx::FromRow, SimpleObject, Debug, Clone)]
#[graphql(complex)] #[graphql(complex)]
pub struct Rolle { pub struct Rolle {
/// Die uuid einer Rolle /// Die uuid einer Rolle
@ -25,26 +26,29 @@ pub struct Rolle {
#[ComplexObject] #[ComplexObject]
impl Rolle { impl Rolle {
/// Die Gruppen in einer Rolle /// Die Gruppen in einer Rolle
pub async fn gruppen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult<Vec<Gruppe>> { pub async fn gruppen<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult<Option<Vec<Gruppe>>> {
let pool = ctx.data::<PgPool>()?; let loader = ctx.data::<LoaderContext>()?;
Ok(loader.rollen_gruppen.load_one(self.id).await?)
let rows = sqlx::query_as!( //
Gruppe, // let pool = ctx.data::<PgPool>()?;
r#" //
SELECT // let rows = sqlx::query_as!(
g.id, // Gruppe,
g.gruppenname, // r#"
g.erstellt_am, // SELECT
g.geaendert_am // g.id,
FROM gruppen AS g // g.gruppenname,
LEFT JOIN rollen_gruppen AS rg ON g.id = rg.gruppe_id // g.erstellt_am,
WHERE rg.rolle_id = $1; // g.geaendert_am
"#, // FROM gruppen AS g
&self.id // LEFT JOIN rollen_gruppen AS rg ON g.id = rg.gruppe_id
) // WHERE rg.rolle_id = $1;
.fetch_all(pool) // "#,
.await?; // &self.id
// )
Ok(rows) // .fetch_all(pool)
// .await?;
//
// Ok(rows)
} }
} }

View File

@ -1,6 +1,7 @@
pub mod hersteller; pub mod hersteller;
pub mod modell; pub mod modell;
pub mod typ; pub mod typ;
use async_graphql::MergedObject; use async_graphql::MergedObject;
#[derive(MergedObject, Default)] #[derive(MergedObject, Default)]