refactor Ulid und Dataloader

This commit is contained in:
2026-06-06 11:59:20 +02:00
parent 581c8bee88
commit b5182b7f19
54 changed files with 364 additions and 301 deletions

View File

@@ -1 +1,3 @@
DROP DOMAIN ULID CASCADE; DROP DOMAIN ulid CASCADE;
DROP EXTENSION IF EXISTS ltree;
DROP EXTENSION IF EXISTS "uuid-ossp";

View File

@@ -1 +1,3 @@
CREATE DOMAIN ULID AS CHAR(26); CREATE DOMAIN ulid AS CHAR(26);
CREATE EXTENSION IF NOT EXISTS ltree;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

View File

@@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS hersteller ( CREATE TABLE IF NOT EXISTS hersteller (
hersteller_id UUID PRIMARY KEY, hersteller_id UUID PRIMARY KEY,
id CHAR(26) UNIQUE NOT NULL, id ULID UNIQUE NOT NULL,
herstellername VARCHAR NOT NULL, herstellername VARCHAR NOT NULL,
erstellt_am TIMESTAMP WITH TIME ZONE NOT NULL, erstellt_am TIMESTAMP WITH TIME ZONE NOT NULL,
geaendert_am TIMESTAMP WITH TIME ZONE NOT NULL geaendert_am TIMESTAMP WITH TIME ZONE NOT NULL

View File

@@ -1,5 +1,6 @@
CREATE TABLE IF NOT EXISTS gruppen ( CREATE TABLE IF NOT EXISTS gruppen (
id UUID PRIMARY KEY, gruppe_id UUID PRIMARY KEY,
id ULID UNIQUE NOT NULL,
gruppenname VARCHAR NOT NULL, gruppenname VARCHAR NOT NULL,
erstellt_am TIMESTAMP WITH TIME ZONE NOT NULL, erstellt_am TIMESTAMP WITH TIME ZONE NOT NULL,
geaendert_am TIMESTAMP WITH TIME ZONE NOT NULL geaendert_am TIMESTAMP WITH TIME ZONE NOT NULL

View File

@@ -1,5 +1,6 @@
CREATE TABLE IF NOT EXISTS rollen ( CREATE TABLE IF NOT EXISTS rollen (
id UUID PRIMARY KEY, rolle_id UUID PRIMARY KEY,
id ULID UNIQUE NOT NULL,
rollenname VARCHAR NOT NULL, rollenname VARCHAR NOT NULL,
erstellt_am TIMESTAMP WITH TIME ZONE NOT NULL, erstellt_am TIMESTAMP WITH TIME ZONE NOT NULL,
geaendert_am TIMESTAMP WITH TIME ZONE NOT NULL geaendert_am TIMESTAMP WITH TIME ZONE NOT NULL

View File

@@ -5,12 +5,12 @@ CREATE TABLE IF NOT EXISTS rollen_gruppen (
CONSTRAINT pk_rollen_gruppen PRIMARY KEY (rolle_id, gruppe_id), CONSTRAINT pk_rollen_gruppen PRIMARY KEY (rolle_id, gruppe_id),
CONSTRAINT fk_rollen_gruppen_gruppe FOREIGN KEY (gruppe_id) CONSTRAINT fk_rollen_gruppen_gruppe FOREIGN KEY (gruppe_id)
REFERENCES gruppen (id) MATCH SIMPLE REFERENCES gruppen (gruppe_id) MATCH SIMPLE
ON UPDATE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE, ON DELETE CASCADE,
CONSTRAINT fk_rollen_gruppen_rolle FOREIGN KEY (rolle_id) CONSTRAINT fk_rollen_gruppen_rolle FOREIGN KEY (rolle_id)
REFERENCES rollen (id) MATCH SIMPLE REFERENCES rollen (rolle_id) MATCH SIMPLE
ON UPDATE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE ON DELETE CASCADE
) )

View File

@@ -1,5 +1,6 @@
CREATE TABLE IF NOT EXISTS benutzer ( CREATE TABLE IF NOT EXISTS benutzer (
id UUID PRIMARY KEY, benutzer_id UUID PRIMARY KEY,
id ULID UNIQUE NOT NULL,
kennung VARCHAR NOT NULL, kennung VARCHAR NOT NULL,
nachname VARCHAR NOT NULL, nachname VARCHAR NOT NULL,
vorname VARCHAR NOT NULL, vorname VARCHAR NOT NULL,

View File

@@ -5,12 +5,12 @@ CREATE TABLE IF NOT EXISTS benutzer_gruppen (
CONSTRAINT pk_benutzer_gruppen PRIMARY KEY (benutzer_id, gruppe_id), CONSTRAINT pk_benutzer_gruppen PRIMARY KEY (benutzer_id, gruppe_id),
CONSTRAINT fk_benutzer_gruppen_benutzer FOREIGN KEY (benutzer_id) CONSTRAINT fk_benutzer_gruppen_benutzer FOREIGN KEY (benutzer_id)
REFERENCES benutzer (id) MATCH SIMPLE REFERENCES benutzer (benutzer_id) MATCH SIMPLE
ON UPDATE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE, ON DELETE CASCADE,
CONSTRAINT fk_benutzer_gruppen_gruppe FOREIGN KEY (gruppe_id) CONSTRAINT fk_benutzer_gruppen_gruppe FOREIGN KEY (gruppe_id)
REFERENCES gruppen (id) MATCH SIMPLE REFERENCES gruppen (gruppe_id) MATCH SIMPLE
ON UPDATE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE ON DELETE CASCADE
) )

View File

@@ -5,12 +5,12 @@ CREATE TABLE IF NOT EXISTS benutzer_rollen (
CONSTRAINT pk_benutzer_rollen PRIMARY KEY (benutzer_id, rolle_id), CONSTRAINT pk_benutzer_rollen PRIMARY KEY (benutzer_id, rolle_id),
CONSTRAINT fk_benutzer_rollen_benutzer FOREIGN KEY (benutzer_id) CONSTRAINT fk_benutzer_rollen_benutzer FOREIGN KEY (benutzer_id)
REFERENCES benutzer (id) MATCH SIMPLE REFERENCES benutzer (benutzer_id) MATCH SIMPLE
ON UPDATE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE, ON DELETE CASCADE,
CONSTRAINT fk_benutzer_rollen_rolle FOREIGN KEY (rolle_id) CONSTRAINT fk_benutzer_rollen_rolle FOREIGN KEY (rolle_id)
REFERENCES rollen (id) MATCH SIMPLE REFERENCES rollen (rolle_id) MATCH SIMPLE
ON UPDATE CASCADE ON UPDATE CASCADE
ON DELETE CASCADE ON DELETE CASCADE
) )

View File

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

View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS strukturen (
id UUID PRIMARY KEY,
parent_id UUID REFERENCES strukturen (id),
dienststelle VARCHAR NOT NULL,
dienststelle_abk VARCHAR NOT NULL,
slug TEXT NOT NULL,
pfad LTREE
);

View File

@@ -1,4 +1,4 @@
pub mod benutzer; // pub mod benutzer;
pub mod gruppe; pub mod gruppe;
pub mod liegenschaft; pub mod liegenschaft;
pub mod rolle; pub mod rolle;

View File

@@ -3,14 +3,14 @@ use async_graphql::{ComplexObject, Context, FieldResult, SimpleObject};
use crate::{ use crate::{
dataloader::LoaderContext, dataloader::LoaderContext,
domain::{gruppe::model::Gruppe, rolle::model::Rolle}, domain::{gruppe::model::Gruppe, rolle::model::Rolle},
scalar::Id, scalar::Ulid,
}; };
#[derive(sqlx::FromRow, SimpleObject)] #[derive(sqlx::FromRow, SimpleObject)]
#[graphql(complex)] #[graphql(complex)]
pub struct Benutzer { pub struct Benutzer {
/// Die UUID eines Benutzers /// Die ULid eines Benutzers
pub id: Id, pub id: Ulid,
/// Die Kennung des Benutzers /// Die Kennung des Benutzers
pub kennung: String, pub kennung: String,

View File

@@ -1,12 +1,13 @@
use crate::scalar::Id;
use async_graphql::InputObject; use async_graphql::InputObject;
use crate::scalar::Ulid;
#[derive(InputObject)] #[derive(InputObject)]
pub struct BenutzerCreateInput { pub struct BenutzerCreateInput {
// #[graphql(validator(min_length = 6, max_length = 8))] // #[graphql(validator(min_length = 6, max_length = 8))]
pub kennung: String, pub kennung: String,
pub vorname: String, pub vorname: String,
pub nachname: String, pub nachname: String,
pub rollen: Option<Vec<Id>>, pub rollen: Option<Vec<Ulid>>,
pub gruppen: Option<Vec<Id>>, pub gruppen: Option<Vec<Ulid>>,
} }

View File

@@ -1,7 +1,7 @@
use async_graphql::{ComplexObject, Enum, SimpleObject}; use async_graphql::{ComplexObject, Enum, SimpleObject};
use sqlx::Type; use sqlx::Type;
use crate::scalar::{Id, Time}; use crate::scalar::{Time, Ulid};
/// Aus welcher Quelle die Gruppe stammt. /// Aus welcher Quelle die Gruppe stammt.
#[derive(Enum, Clone, Debug, Eq, Copy, PartialEq, Type)] #[derive(Enum, Clone, Debug, Eq, Copy, PartialEq, Type)]
@@ -21,7 +21,7 @@ pub enum Herkunft {
#[graphql(complex)] #[graphql(complex)]
pub struct GruppeAnsicht { pub struct GruppeAnsicht {
/// Die UUID einer Gruppe /// Die UUID einer Gruppe
pub id: Id, pub id: Ulid,
/// Der Gruppenname /// Der Gruppenname
pub gruppenname: String, pub gruppenname: String,

View File

@@ -5,17 +5,17 @@ use std::sync::Arc;
use crate::domain::gruppe::model::Gruppe; use crate::domain::gruppe::model::Gruppe;
use crate::domain::gruppe::service::Service; use crate::domain::gruppe::service::Service;
use crate::scalar::Id; use crate::scalar::Ulid;
pub struct GruppenLoader { pub struct GruppenLoader {
pub pool: sqlx::PgPool, pub pool: sqlx::PgPool,
} }
impl Loader<Id> for GruppenLoader { impl Loader<Ulid> for GruppenLoader {
type Value = Vec<Gruppe>; type Value = Vec<Gruppe>;
type Error = Arc<sqlx::Error>; type Error = Arc<sqlx::Error>;
async fn load(&self, keys: &[Id]) -> Result<HashMap<Id, Self::Value>, Self::Error> { async fn load(&self, keys: &[Ulid]) -> Result<HashMap<Ulid, Self::Value>, Self::Error> {
let rows = Service::new(self.pool.clone()) let rows = Service::new(self.pool.clone())
.gruppe_dataloader(keys) .gruppe_dataloader(keys)
.await?; .await?;

View File

@@ -1,2 +1,6 @@
pub mod gruppe; pub mod gruppe;
pub use gruppe::Gruppe;
pub use gruppe::GruppeDataloader;
pub use gruppe::GruppeErstellen;
pub use gruppe::GruppeLoeschen;
pub use gruppe::GruppeUpdate;

View File

@@ -1,8 +1,30 @@
use crate::scalar::{Id, Time}; use sqlx::FromRow;
pub struct Gruppe { use crate::scalar::{Id, Time, Ulid};
pub id: Id,
pub struct GruppeErstellen {
pub gruppe_id: Id,
pub id: Ulid,
pub gruppenname: String, pub gruppenname: String,
pub erstellt_am: Option<Time>, pub erstellt_am: Time,
pub geaendert_am: Option<Time>, pub geaendert_am: Time,
}
pub struct GruppeLoeschen {
pub id: Ulid,
}
pub struct GruppeUpdate {
pub id: Ulid,
pub gruppenname: String,
pub geaendert_am: Time,
}
#[derive(Debug, FromRow)]
pub struct GruppeDataloader {
pub r_ulid: Ulid,
pub g_ulid: Ulid,
pub gruppenname: String,
pub erstellt_am: Time,
pub geaendert_am: Time,
} }

View File

@@ -1,12 +1,12 @@
use async_graphql::{ComplexObject, SimpleObject}; use async_graphql::{ComplexObject, SimpleObject};
use crate::scalar::{Id, Time}; use crate::scalar::{Time, Ulid};
#[derive(sqlx::FromRow, SimpleObject, Clone, Debug)] #[derive(sqlx::FromRow, SimpleObject, Clone, Debug)]
#[graphql(complex)] #[graphql(complex)]
pub struct Gruppe { pub struct Gruppe {
/// Die UUID einer Gruppe /// Die UUID einer Gruppe
pub id: Id, pub id: Ulid,
/// Der Gruppenname /// Der Gruppenname
pub gruppenname: String, pub gruppenname: String,

View File

@@ -1,9 +1,9 @@
use async_graphql::InputObject; use async_graphql::InputObject;
use crate::scalar::Id; use crate::scalar::Ulid;
#[derive(InputObject)] #[derive(InputObject)]
pub struct GruppeLoeschenInput { pub struct GruppeLoeschenInput {
/// Die ID einer Gruppe /// Die ULID einer Gruppe
pub id: Id, pub id: Ulid,
} }

View File

@@ -1,10 +1,10 @@
use crate::scalar::Id; use crate::scalar::Ulid;
use async_graphql::InputObject; use async_graphql::InputObject;
#[derive(InputObject)] #[derive(InputObject)]
pub struct GruppeUpdateInput { pub struct GruppeUpdateInput {
/// Die ID einer Gruppe /// Die Ulid einer Gruppe
pub id: Id, pub id: Ulid,
/// Der Name einer Gruppe /// Der Name einer Gruppe
pub gruppenname: String, pub gruppenname: String,

View File

@@ -1,48 +1,53 @@
use itertools::Itertools;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use itertools::Itertools;
use super::Repository; use super::Repository;
use crate::database::Queryer; use crate::database::Queryer;
use crate::domain::gruppe::entity::GruppeDataloader;
use crate::domain::gruppe::model::Gruppe; use crate::domain::gruppe::model::Gruppe;
use crate::scalar::Id; use crate::scalar::Ulid;
impl Repository { impl Repository {
pub async fn gruppe_dataloader<'c, C: Queryer<'c>>( pub async fn gruppe_dataloader<'c, C: Queryer<'c>>(
&self, &self,
db: C, db: C,
keys: &[Id], keys: &[Ulid],
) -> Result<HashMap<Id, Vec<Gruppe>>, Arc<sqlx::Error>> { ) -> Result<HashMap<Ulid, Vec<Gruppe>>, Arc<sqlx::Error>> {
let rows = sqlx::query!( const QUERY: &str = r#"
r#" SELECT
SELECT rg.rolle_id as r_ulid,
rg.rolle_id, g.id as g_ulid,
g.id, g.gruppenname,
g.gruppenname, g.erstellt_am,
g.erstellt_am, g.geaendert_am
g.geaendert_am FROM gruppen AS g
FROM gruppen AS g LEFT JOIN rollen_gruppen AS rg ON g.gruppe_id = rg.gruppe_id
LEFT JOIN rollen_gruppen AS rg ON g.id = rg.gruppe_id WHERE rg.rolle_id IN (
WHERE rg.rolle_id = ANY($1); SELECT benutzer_id
"#, FROM benutzer
keys WHERE id = ANY($1)
) );
.fetch_all(db) "#;
.await?
.into_iter() let rows = sqlx::query_as::<_, GruppeDataloader>(QUERY)
.map(|row| { .bind(keys)
( .fetch_all(db)
row.rolle_id, .await?
Gruppe { .into_iter()
id: row.id, .map(|row| {
gruppenname: row.gruppenname, (
erstellt_am: row.erstellt_am, row.r_ulid,
geaendert_am: row.geaendert_am, Gruppe {
}, id: row.g_ulid,
) gruppenname: row.gruppenname,
}) erstellt_am: row.erstellt_am,
.into_group_map(); geaendert_am: row.geaendert_am,
},
)
})
.into_group_map();
Ok(rows) Ok(rows)
} }

View File

@@ -8,19 +8,20 @@ impl Repository {
pub async fn gruppe_erstellen<'c, C: Queryer<'c>>( pub async fn gruppe_erstellen<'c, C: Queryer<'c>>(
&self, &self,
db: C, db: C,
gruppe: &entity::Gruppe, gruppe: &entity::GruppeErstellen,
) -> Result<model::Gruppe, Error> { ) -> Result<model::Gruppe, Error> {
const QUERY: &str = r#" const QUERY: &str = r#"
INSERT INTO gruppen (id, erstellt_am, geaendert_am, gruppenname) VALUES ( INSERT INTO gruppen (gruppe_id, id, gruppenname, erstellt_am, geaendert_am) VALUES (
$1, $2, $3, $4 $1, $2, $3, $4, $5
) RETURNING id, erstellt_am, geaendert_am, gruppenname; ) RETURNING id, gruppenname, erstellt_am, geaendert_am;
"#; "#;
let gruppe = sqlx::query_as::<_, model::Gruppe>(QUERY) let gruppe = sqlx::query_as::<_, model::Gruppe>(QUERY)
.bind(gruppe.gruppe_id)
.bind(gruppe.id) .bind(gruppe.id)
.bind(&gruppe.gruppenname)
.bind(gruppe.erstellt_am) .bind(gruppe.erstellt_am)
.bind(gruppe.geaendert_am) .bind(gruppe.geaendert_am)
.bind(&gruppe.gruppenname)
.fetch_one(db) .fetch_one(db)
.await?; .await?;

View File

@@ -8,7 +8,7 @@ impl Repository {
pub async fn gruppe_loeschen<'c, C: Queryer<'c>>( pub async fn gruppe_loeschen<'c, C: Queryer<'c>>(
&self, &self,
db: C, db: C,
gruppe: &entity::Gruppe, gruppe: &entity::GruppeLoeschen,
) -> Result<model::Gruppe, Error> { ) -> Result<model::Gruppe, Error> {
const QUERY: &str = r#" const QUERY: &str = r#"
DELETE FROM gruppen WHERE id=$1 RETURNING id, gruppenname, erstellt_am, geaendert_am; DELETE FROM gruppen WHERE id=$1 RETURNING id, gruppenname, erstellt_am, geaendert_am;

View File

@@ -8,12 +8,12 @@ impl Repository {
pub async fn gruppe_update<'c, C: Queryer<'c>>( pub async fn gruppe_update<'c, C: Queryer<'c>>(
&self, &self,
db: C, db: C,
gruppe: &entity::Gruppe, gruppe: &entity::GruppeUpdate,
) -> Result<model::Gruppe, Error> { ) -> Result<model::Gruppe, Error> {
const QUERY: &str = r#" const QUERY: &str = r#"
UPDATE gruppen UPDATE gruppen
SET geaendert_am = $2, ruppenname = $3 WHERE gruppeid = $1 SET geaendert_am = $2, ruppenname = $3 WHERE gruppeid = $1
RETURNING id, geaendert_am, erstellt_am, gruppenname; RETURNING id, gruppenname, geaendert_am, erstellt_am;
"#; "#;
let gruppe = sqlx::query_as::<_, model::Gruppe>(QUERY) let gruppe = sqlx::query_as::<_, model::Gruppe>(QUERY)

View File

@@ -1,14 +1,14 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use crate::{domain::gruppe::model::Gruppe, scalar::Id}; use crate::{domain::gruppe::model::Gruppe, scalar::Ulid};
use super::Service; use super::Service;
impl Service { impl Service {
pub async fn gruppe_dataloader( pub async fn gruppe_dataloader(
&self, &self,
keys: &[Id], keys: &[Ulid],
) -> Result<HashMap<Id, Vec<Gruppe>>, Arc<sqlx::Error>> { ) -> Result<HashMap<Ulid, Vec<Gruppe>>, Arc<sqlx::Error>> {
let gruppen_dataloader = self.repo.gruppe_dataloader(&self.db, keys).await?; let gruppen_dataloader = self.repo.gruppe_dataloader(&self.db, keys).await?;
Ok(gruppen_dataloader) Ok(gruppen_dataloader)
} }

View File

@@ -1,23 +1,25 @@
use super::Service;
use crate::{
domain::gruppe::{
entity,
model::{self, GruppeErstelleInput},
},
scalar::Ulid,
};
use anyhow::Error; use anyhow::Error;
use chrono::Utc; use chrono::Utc;
use ulid::Ulid;
use super::Service;
use crate::domain::gruppe::{
entity,
model::{self, GruppeErstelleInput},
};
impl Service { impl Service {
pub async fn gruppe_erstellen( pub async fn gruppe_erstellen(
&self, &self,
input: GruppeErstelleInput, input: GruppeErstelleInput,
) -> Result<model::Gruppe, Error> { ) -> Result<model::Gruppe, Error> {
let gruppe_input = entity::Gruppe { let gruppe_input = entity::GruppeErstellen {
id: Ulid::new().into(), gruppe_id: ulid::Ulid::new().into(),
id: Ulid(ulid::Ulid::new().into()),
gruppenname: input.gruppenname, gruppenname: input.gruppenname,
erstellt_am: Some(Utc::now()), erstellt_am: Utc::now(),
geaendert_am: Some(Utc::now()), geaendert_am: Utc::now(),
}; };
let gruppe = self.repo.gruppe_erstellen(&self.db, &gruppe_input).await?; let gruppe = self.repo.gruppe_erstellen(&self.db, &gruppe_input).await?;

View File

@@ -1,5 +1,4 @@
use anyhow::Error; use anyhow::Error;
use chrono::Utc;
use super::Service; use super::Service;
use crate::domain::gruppe::{ use crate::domain::gruppe::{
@@ -12,12 +11,7 @@ impl Service {
&self, &self,
input: GruppeLoeschenInput, input: GruppeLoeschenInput,
) -> Result<model::Gruppe, Error> { ) -> Result<model::Gruppe, Error> {
let gruppe_input = entity::Gruppe { let gruppe_input = entity::GruppeLoeschen { id: input.id };
id: input.id,
gruppenname: String::new(),
erstellt_am: None,
geaendert_am: Some(Utc::now()),
};
let gruppe = self.repo.gruppe_loeschen(&self.db, &gruppe_input).await?; let gruppe = self.repo.gruppe_loeschen(&self.db, &gruppe_input).await?;
Ok(gruppe) Ok(gruppe)

View File

@@ -1,6 +1,5 @@
use anyhow::Error; use anyhow::Error;
use chrono::Utc; use chrono::Utc;
use ulid::Ulid;
use super::Service; use super::Service;
use crate::domain::gruppe::{ use crate::domain::gruppe::{
@@ -10,11 +9,10 @@ use crate::domain::gruppe::{
impl Service { impl Service {
pub async fn gruppe_update(&self, input: GruppeUpdateInput) -> Result<model::Gruppe, Error> { pub async fn gruppe_update(&self, input: GruppeUpdateInput) -> Result<model::Gruppe, Error> {
let gruppe_input = entity::Gruppe { let gruppe_input = entity::GruppeUpdate {
id: Ulid::new().into(), id: input.id,
gruppenname: input.gruppenname, gruppenname: input.gruppenname,
erstellt_am: None, geaendert_am: Utc::now(),
geaendert_am: Some(Utc::now()),
}; };
let gruppe = self.repo.gruppe_update(&self.db, &gruppe_input).await?; let gruppe = self.repo.gruppe_update(&self.db, &gruppe_input).await?;

View File

@@ -1,51 +1,23 @@
use async_graphql::dataloader::*; use async_graphql::dataloader::Loader;
use async_graphql::*;
use itertools::Itertools;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use crate::domain::rolle::model::Rolle; use crate::domain::rolle::model::Rolle;
use crate::scalar::Id; use crate::domain::rolle::service::Service;
use crate::scalar::Ulid;
pub struct RollenLoader { pub struct RollenLoader {
pub pool: sqlx::PgPool, pub pool: sqlx::PgPool,
} }
impl Loader<Id> for RollenLoader { impl Loader<Ulid> for RollenLoader {
type Value = Vec<Rolle>; type Value = Vec<Rolle>;
type Error = Arc<sqlx::Error>; type Error = Arc<sqlx::Error>;
async fn load(&self, keys: &[Id]) -> Result<HashMap<Id, Self::Value>, Self::Error> { async fn load(&self, keys: &[Ulid]) -> Result<HashMap<Ulid, Self::Value>, Self::Error> {
let rows = sqlx::query!( let rows = Service::new(self.pool.clone())
r#" .rolle_dataloader(keys)
SELECT .await?;
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) Ok(rows)
} }
} }

View File

@@ -1,2 +1,6 @@
pub mod rolle; pub mod rolle;
pub use rolle::Rolle;
pub use rolle::RolleErstellen;
pub use rolle::RolleLoeschen;
pub use rolle::RolleRow;
pub use rolle::RolleUpdate;

View File

@@ -1,8 +1,30 @@
use crate::scalar::{Id, Time}; use sqlx::FromRow;
pub struct Rolle { use crate::scalar::{Id, Time, Ulid};
pub id: Id,
pub struct RolleErstellen {
pub rolle_id: Id,
pub id: Ulid,
pub rollenname: String, pub rollenname: String,
pub erstellt_am: Option<Time>, pub erstellt_am: Time,
pub geaendert_am: Option<Time>, pub geaendert_am: Time,
}
pub struct RolleLoeschen {
pub id: Ulid,
}
pub struct RolleUpdate {
pub id: Ulid,
pub rollenname: String,
pub geaendert_am: Time,
}
#[derive(Debug, FromRow)]
pub struct RolleRow {
pub b_ulid: Ulid,
pub r_ulid: Ulid,
pub rollenname: String,
pub erstellt_am: Time,
pub geaendert_am: Time,
} }

View File

@@ -3,15 +3,15 @@ use async_graphql::{ComplexObject, Context, FieldResult, SimpleObject};
use crate::{ use crate::{
dataloader::LoaderContext, dataloader::LoaderContext,
domain::gruppe::model::Gruppe, domain::gruppe::model::Gruppe,
scalar::{Id, Time}, scalar::{Time, Ulid},
}; };
/// Um die Administration zu erleichtern werden Gruppen in die Rollen hinzugefuegt /// Um die Administration zu erleichtern werden Gruppen in die Rollen hinzugefuegt
#[derive(sqlx::FromRow, SimpleObject, Debug, Clone)] #[derive(sqlx::FromRow, SimpleObject, Debug, Clone)]
#[graphql(complex)] #[graphql(complex)]
pub struct Rolle { pub struct Rolle {
/// Die uuid einer Rolle /// Die ULID einer Rolle
pub id: Id, pub id: Ulid,
/// Der Rollenname /// Der Rollenname
pub rollenname: String, pub rollenname: String,

View File

@@ -1,9 +1,9 @@
use async_graphql::InputObject; use async_graphql::InputObject;
use crate::scalar::Id; use crate::scalar::Ulid;
#[derive(InputObject)] #[derive(InputObject)]
pub struct RolleLoeschenInput { pub struct RolleLoeschenInput {
/// Die ID einer Rolle /// Die Ulid einer Rolle
pub id: Id, pub id: Ulid,
} }

View File

@@ -1,10 +1,10 @@
use crate::scalar::Id; use crate::scalar::Ulid;
use async_graphql::InputObject; use async_graphql::InputObject;
#[derive(InputObject)] #[derive(InputObject)]
pub struct RolleUpdateInput { pub struct RolleUpdateInput {
/// Die ID einer Rolle /// Die Ulid einer Rolle
pub id: Id, pub id: Ulid,
/// Der Name einer Rolle /// Der Name einer Rolle
pub rollenname: String, pub rollenname: String,

View File

@@ -1,7 +1,7 @@
use async_graphql::{Context, FieldResult, Object}; use async_graphql::{Context, FieldResult, Object};
use sqlx::postgres::PgPool; use sqlx::postgres::PgPool;
use crate::domain::rolle::model::Rolle; use crate::domain::rolle::{model::Rolle, service::Service};
#[derive(Default)] #[derive(Default)]
pub struct RolleQuery {} pub struct RolleQuery {}
@@ -15,11 +15,8 @@ impl RolleQuery {
// } // }
async fn rollen(&self, ctx: &Context<'_>) -> FieldResult<Vec<Rolle>> { async fn rollen(&self, ctx: &Context<'_>) -> FieldResult<Vec<Rolle>> {
let pool = ctx.data::<PgPool>()?; let pool = ctx.data::<PgPool>()?.clone();
let rollen = Service::new(pool).rolle_alle().await?;
let rollen = sqlx::query_as!(Rolle, "SELECT * FROM rollen")
.fetch_all(pool)
.await?;
Ok(rollen) Ok(rollen)
} }

View File

@@ -1,9 +1,10 @@
mod find_all_rolle; mod find_all_rolle;
mod find_rolle_by_id; mod find_rolle_by_id;
mod rolle_alle;
mod rolle_dataloader;
mod rolle_erstellen; mod rolle_erstellen;
mod rolle_loeschen; mod rolle_loeschen;
mod rolle_update; mod rolle_update;
mod rollen_dataloader;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Repository {} pub struct Repository {}
@@ -13,6 +14,7 @@ impl Repository {
Repository {} Repository {}
} }
} }
impl Default for Repository { impl Default for Repository {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()

View File

@@ -0,0 +1,17 @@
use anyhow::Error;
use super::Repository;
use crate::{database::Queryer, domain::rolle::model};
impl Repository {
pub async fn rolle_alle<'c, C: Queryer<'c>>(&self, db: C) -> Result<Vec<model::Rolle>, Error> {
const QUERY: &str = r#"
SELECT id, rollenname, erstellt_am, geaendert_am FROM rollen
"#;
let rollen = sqlx::query_as::<_, model::Rolle>(QUERY)
.fetch_all(db)
.await?;
Ok(rollen)
}
}

View File

@@ -0,0 +1,55 @@
use itertools::Itertools;
use std::collections::HashMap;
use std::sync::Arc;
use super::Repository;
use crate::database::Queryer;
use crate::domain::rolle::entity::RolleRow;
use crate::domain::rolle::model;
use crate::scalar::Ulid;
impl Repository {
pub async fn rolle_dataloader<'c, C: Queryer<'c>>(
&self,
db: C,
keys: &[Ulid],
) -> Result<HashMap<Ulid, Vec<model::Rolle>>, Arc<sqlx::Error>> {
const QUERY: &str = r#"
SELECT
b.id as b_ulid,
r.id as r_ulid,
r.rollenname,
r.erstellt_am,
r.geaendert_am
FROM rollen AS r
LEFT JOIN benutzer_rollen AS br ON r.rolle_id = br.rolle_id
LEFT JOIN benutzer AS b ON br.benutzer_id = b.benutzer_id
WHERE br.benutzer_id IN (
SELECT benutzer_id
FROM benutzer
WHERE id = ANY($1)
)
"#;
let rows = sqlx::query_as::<_, RolleRow>(QUERY)
.bind(keys)
.fetch_all(db)
.await?
.into_iter()
.map(|row| {
(
row.b_ulid,
model::Rolle {
id: row.r_ulid,
rollenname: row.rollenname,
erstellt_am: row.erstellt_am,
geaendert_am: row.geaendert_am,
},
)
})
.into_group_map();
Ok(rows)
}
}

View File

@@ -8,19 +8,20 @@ impl Repository {
pub async fn rolle_erstellen<'c, C: Queryer<'c>>( pub async fn rolle_erstellen<'c, C: Queryer<'c>>(
&self, &self,
db: C, db: C,
rolle: &entity::Rolle, rolle: &entity::RolleErstellen,
) -> Result<model::Rolle, Error> { ) -> Result<model::Rolle, Error> {
const QUERY: &str = r#" const QUERY: &str = r#"
INSERT INTO rollen (id, erstellt_am, geaendert_am, rollenname) VALUES ( INSERT INTO rollen (rolle_id, id, rollenname, erstellt_am, geaendert_am) VALUES (
$1, $2, $3, $4 $1, $2, $3, $4, $5
) RETURNING id, erstellt_am, geaendert_am, rollenname ) RETURNING id, rollenname, erstellt_am, geaendert_am
"#; "#;
let rolle = sqlx::query_as::<_, model::Rolle>(QUERY) let rolle = sqlx::query_as::<_, model::Rolle>(QUERY)
.bind(rolle.rolle_id)
.bind(rolle.id) .bind(rolle.id)
.bind(&rolle.rollenname)
.bind(rolle.erstellt_am) .bind(rolle.erstellt_am)
.bind(rolle.geaendert_am) .bind(rolle.geaendert_am)
.bind(&rolle.rollenname)
.fetch_one(db) .fetch_one(db)
.await?; .await?;

View File

@@ -10,7 +10,7 @@ impl Repository {
pub async fn rolle_loeschen<'c, C: Queryer<'c>>( pub async fn rolle_loeschen<'c, C: Queryer<'c>>(
&self, &self,
db: C, db: C,
rolle: &entity::Rolle, rolle: &entity::RolleLoeschen,
) -> Result<model::Rolle, Error> { ) -> Result<model::Rolle, Error> {
const QUERY: &str = r#" const QUERY: &str = r#"
DELETE FROM rollen WHERE id=$1 RETURNING id, rollenname, erstellt_am, geaendert_am; DELETE FROM rollen WHERE id=$1 RETURNING id, rollenname, erstellt_am, geaendert_am;

View File

@@ -8,7 +8,7 @@ impl Repository {
pub async fn rolle_update<'c, C: Queryer<'c>>( pub async fn rolle_update<'c, C: Queryer<'c>>(
&self, &self,
db: C, db: C,
rolle: &entity::Rolle, rolle: &entity::RolleUpdate,
) -> Result<model::Rolle, Error> { ) -> Result<model::Rolle, Error> {
const QUERY: &str = r#" const QUERY: &str = r#"
UPDATE rollen UPDATE rollen

View File

@@ -1,49 +0,0 @@
use std::collections::HashMap;
use std::sync::Arc;
use itertools::Itertools;
use super::Repository;
use crate::database::Queryer;
use crate::domain::rolle::model::Rolle;
use crate::scalar::Id;
impl Repository {
pub async fn rollen_dataloader<'c, C: Queryer<'c>>(
&self,
db: C,
keys: &[Id],
) -> Result<HashMap<Id, Vec<Rolle>>, Arc<sqlx::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(db)
.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)
}
}

View File

@@ -1,3 +1,5 @@
mod rolle_alle;
mod rolle_dataloader;
mod rolle_erstellen; mod rolle_erstellen;
mod rolle_loeschen; mod rolle_loeschen;
mod rolle_update; mod rolle_update;

View File

@@ -0,0 +1,12 @@
use anyhow::Error;
use crate::domain::rolle::model;
use super::Service;
impl Service {
pub async fn rolle_alle(&self) -> Result<Vec<model::Rolle>, Error> {
let typen = self.repo.rolle_alle(&self.db).await?;
Ok(typen)
}
}

View File

@@ -0,0 +1,15 @@
use std::{collections::HashMap, sync::Arc};
use crate::{domain::rolle::model, scalar::Ulid};
use super::Service;
impl Service {
pub async fn rolle_dataloader(
&self,
keys: &[Ulid],
) -> Result<HashMap<Ulid, Vec<model::Rolle>>, Arc<sqlx::Error>> {
let typen = self.repo.rolle_dataloader(&self.db, keys).await?;
Ok(typen)
}
}

View File

@@ -1,20 +1,23 @@
use anyhow::Error; use anyhow::Error;
use chrono::Utc; use chrono::Utc;
use ulid::Ulid;
use super::Service; use super::Service;
use crate::domain::rolle::{ use crate::{
entity, domain::rolle::{
model::{self, RolleErstelleInput}, entity,
model::{self, RolleErstelleInput},
},
scalar::Ulid,
}; };
impl Service { impl Service {
pub async fn rolle_erstellen(&self, input: RolleErstelleInput) -> Result<model::Rolle, Error> { pub async fn rolle_erstellen(&self, input: RolleErstelleInput) -> Result<model::Rolle, Error> {
let rolle_input = entity::Rolle { let rolle_input = entity::RolleErstellen {
id: Ulid::new().into(), rolle_id: ulid::Ulid::new().into(),
id: Ulid(ulid::Ulid::new()),
rollenname: input.rollenname, rollenname: input.rollenname,
erstellt_am: Some(Utc::now()), erstellt_am: Utc::now(),
geaendert_am: Some(Utc::now()), geaendert_am: Utc::now(),
}; };
let rolle = self.repo.rolle_erstellen(&self.db, &rolle_input).await?; let rolle = self.repo.rolle_erstellen(&self.db, &rolle_input).await?;

View File

@@ -8,12 +8,7 @@ use crate::domain::rolle::{
impl Service { impl Service {
pub async fn rolle_loeschen(&self, input: RolleLoeschenInput) -> Result<model::Rolle, Error> { pub async fn rolle_loeschen(&self, input: RolleLoeschenInput) -> Result<model::Rolle, Error> {
let rolle_input = entity::Rolle { let rolle_input = entity::RolleLoeschen { id: input.id };
id: input.id,
rollenname: String::new(),
erstellt_am: None,
geaendert_am: None,
};
let deleted = self.repo.rolle_loeschen(&self.db, &rolle_input).await?; let deleted = self.repo.rolle_loeschen(&self.db, &rolle_input).await?;
Ok(deleted) Ok(deleted)

View File

@@ -9,11 +9,10 @@ use crate::domain::rolle::{
impl Service { impl Service {
pub async fn rolle_update(&self, input: RolleUpdateInput) -> Result<model::Rolle, Error> { pub async fn rolle_update(&self, input: RolleUpdateInput) -> Result<model::Rolle, Error> {
let rolle_input = entity::Rolle { let rolle_input = entity::RolleUpdate {
id: input.id, id: input.id,
rollenname: input.rollenname, rollenname: input.rollenname,
erstellt_am: None, geaendert_am: Utc::now(),
geaendert_am: Some(Utc::now()),
}; };
let rolle = self.repo.rolle_update(&self.db, &rolle_input).await?; let rolle = self.repo.rolle_update(&self.db, &rolle_input).await?;

View File

@@ -1,6 +1,5 @@
pub mod typ; pub mod typ;
// pub use typ::Typ;
pub use typ::TypErstellen; pub use typ::TypErstellen;
pub use typ::TypLoeschen; pub use typ::TypLoeschen;
pub use typ::TypUpdate; pub use typ::TypUpdate;

View File

@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool}; use sqlx::{FromRow, PgPool};
use ulid::Ulid; use ulid::Ulid;
use crate::scalar::Id; use crate::scalar::{Id, Time};
#[derive(SimpleObject, Debug, FromRow, Deserialize, Serialize, sqlx::Type)] #[derive(SimpleObject, Debug, FromRow, Deserialize, Serialize, sqlx::Type)]
pub struct Hersteller { pub struct Hersteller {
@@ -12,7 +12,13 @@ pub struct Hersteller {
pub id: Id, pub id: Id,
/// Der Name eines Herstellers /// Der Name eines Herstellers
herstellername: String, pub herstellername: String,
/// Wann die Rolle erstellt wurde
pub erstellt_am: Time,
/// Wann die Rolle geaendert wurde
pub geaendert_am: Time,
} }
#[derive(InputObject, Debug)] #[derive(InputObject, Debug)]
@@ -40,9 +46,12 @@ impl Hersteller {
} }
pub async fn read_all(pool: &PgPool) -> Result<Vec<Hersteller>> { pub async fn read_all(pool: &PgPool) -> Result<Vec<Hersteller>> {
let rows = sqlx::query_as!(Hersteller, "SELECT id, herstellername FROM hersteller") let rows = sqlx::query_as!(
.fetch_all(pool) Hersteller,
.await?; "SELECT id, herstellername, erstellt_am, geaendert_am FROM hersteller"
)
.fetch_all(pool)
.await?;
Ok(rows) Ok(rows)
} }

View File

@@ -1,4 +1,4 @@
pub mod benutzer; // pub mod benutzer;
pub mod gruppe; pub mod gruppe;
// pub mod hersteller; // pub mod hersteller;
// pub mod modell; // pub mod modell;
@@ -14,7 +14,7 @@ pub struct Mutation(
TypMutation, TypMutation,
// hersteller::HerstellerMutation, // hersteller::HerstellerMutation,
// modell::ModellMutation, // modell::ModellMutation,
benutzer::BenutzerMutation, // benutzer::BenutzerMutation,
gruppe::GruppeMutation, gruppe::GruppeMutation,
rolle::RolleMutation, rolle::RolleMutation,
liegenschaft::LiegenschaftMutation, liegenschaft::LiegenschaftMutation,

View File

@@ -1,8 +1,11 @@
// pub mod hersteller; // pub mod hersteller;
// pub mod modell; // pub mod modell;
use crate::domain::{ use crate::domain::{
benutzer::queries::benutzer, gruppe::queries::gruppe, liegenschaft::queries::liegenschaft, // benutzer::queries::benutzer,
rolle::queries::rolle, typ::queries::typ::TypQuery, gruppe::queries::gruppe,
liegenschaft::queries::liegenschaft,
rolle::queries::rolle,
typ::queries::typ::TypQuery,
}; };
use async_graphql::MergedObject; use async_graphql::MergedObject;
@@ -10,7 +13,7 @@ use async_graphql::MergedObject;
#[derive(MergedObject, Default)] #[derive(MergedObject, Default)]
pub struct Query( pub struct Query(
TypQuery, TypQuery,
benutzer::BenutzerQuery, // benutzer::BenutzerQuery,
rolle::RolleQuery, rolle::RolleQuery,
gruppe::GruppeQuery, gruppe::GruppeQuery,
liegenschaft::LiegenschaftQuery, // modell::ModellQuery, liegenschaft::LiegenschaftQuery, // modell::ModellQuery,

View File

@@ -1,67 +1,26 @@
use async_graphql::*; use async_graphql::*;
/// It is easier to track each type alias if this file is located on the top-level directory (here) use sqlx::{
/// than in each domain. Also, separating them will create a lot of duplicate code. postgres::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef},
use uuid::Uuid; Decode, Encode, Postgres, Type,
};
use ulid::Ulid as UlidPrimitive;
pub type Time = chrono::DateTime<chrono::Utc>; pub type Time = chrono::DateTime<chrono::Utc>;
/// The ID scalar type represents a unique identifier, often used to refetch an object or as key for a cache. /// The ID scalar type represents a unique identifier, often used to refetch an object or as key for a cache.
/// The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. /// The ID type appears in a JSON response as a String; however, it is not intended to be human-readable.
/// When expected as an input type, any string (such as "4") or integer (such as 4) input value will be accepted as an ID. /// When expected as an input type, any string (such as "4") or integer (such as 4) input value will be accepted as an ID.
pub type Id = Uuid; pub type Id = uuid::Uuid;
// pub type Ulid = String; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Ulid(pub UlidPrimitive);
// pub type Ulid = ulid::Ulid;
// #[derive(Clone, Copy, Eq, PartialEq)]
// pub struct Ulid(pub ulid::Ulid);
//
// /// The ULID scalar type represents a Universally Unique Lexicographically Sortable Identifier as defined by the ULID specification.
// /// ULIDs are 26-character strings that are URL-safe, case-insensitive, and lexicographically sortable,
// /// making them ideal for distributed systems requiring time-ordered unique identifiers.
// #[Scalar]
// impl ScalarType for Ulid {
// fn parse(value: Value) -> InputValueResult<Self> {
// match value {
// Value::String(s) => {
// let ulid = ulid::Ulid::from_string(&s).map_err(InputValueError::custom)?;
// Ok(Ulid(ulid))
// }
// _ => Err(InputValueError::expected_type(value)),
// }
// }
//
// fn to_value(&self) -> Value {
// Value::String(self.0.to_string())
// }
// }
//
// impl fmt::Display for Ulid {
// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// write!(f, "{}", self.0) // delegiert an Ulid::to_string()
// }
// }
//
//
//
use sqlx::{
postgres::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef},
Decode, Encode, Postgres, Type,
};
// #[derive(sqlx::Type)]
// #[sqlx(type_name = "ULID")]
// #[sqlx(no_pg_array)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Ulid(pub ulid::Ulid);
#[Scalar] #[Scalar]
impl ScalarType for Ulid { impl ScalarType for Ulid {
fn parse(value: Value) -> InputValueResult<Self> { fn parse(value: Value) -> InputValueResult<Self> {
match value { match value {
Value::String(s) => { Value::String(s) => {
let ulid = ulid::Ulid::from_string(&s).map_err(InputValueError::custom)?; let ulid = UlidPrimitive::from_string(&s).map_err(InputValueError::custom)?;
Ok(Ulid(ulid)) Ok(Ulid(ulid))
} }
_ => Err(InputValueError::expected_type(value)), _ => Err(InputValueError::expected_type(value)),
@@ -75,17 +34,19 @@ impl ScalarType for Ulid {
impl Type<Postgres> for Ulid { impl Type<Postgres> for Ulid {
fn type_info() -> PgTypeInfo { fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("ULID") <String as Type<Postgres>>::type_info()
} }
fn compatible(ty: &PgTypeInfo) -> bool { fn compatible(ty: &PgTypeInfo) -> bool {
<String as Type<Postgres>>::compatible(ty) <String as Type<Postgres>>::compatible(ty)
} }
} }
impl<'r> Decode<'r, Postgres> for Ulid { impl<'r> Decode<'r, Postgres> for Ulid {
fn decode(value: PgValueRef<'r>) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> { fn decode(value: PgValueRef<'r>) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let s = <String as Decode<Postgres>>::decode(value)?; let s = <String as Decode<Postgres>>::decode(value)?;
Ok(Ulid(ulid::Ulid::from_string(s.trim())?)) Ok(Ulid(UlidPrimitive::from_string(s.trim())?))
} }
} }
@@ -97,8 +58,9 @@ impl<'q> Encode<'q, Postgres> for Ulid {
<String as Encode<Postgres>>::encode(self.0.to_string(), buf) <String as Encode<Postgres>>::encode(self.0.to_string(), buf)
} }
} }
impl PgHasArrayType for Ulid { impl PgHasArrayType for Ulid {
fn array_type_info() -> PgTypeInfo { fn array_type_info() -> PgTypeInfo {
PgTypeInfo::with_name("_ULID") <String as PgHasArrayType>::array_type_info()
} }
} }