From a2e10b763077a81dd2d72800cd9ebaa533b07f75 Mon Sep 17 00:00:00 2001 From: John Lewis Date: Thu, 16 Oct 2025 14:15:47 -0700 Subject: [PATCH 1/6] feat(domain): migrate to `MutationService` to guard mutations --- crates/domain/src/create.rs | 31 +------ crates/domain/src/delete_entry.rs | 6 +- crates/domain/src/download/tests.rs | 4 +- crates/domain/src/lib.rs | 27 +++--- crates/domain/src/migrate.rs | 94 +-------------------- crates/domain/src/mutate.rs | 34 ++++++++ crates/domain/src/mutate/create.rs | 72 ++++++++++++++++ crates/domain/src/mutate/delete_entry.rs | 18 ++++ crates/domain/src/mutate/migrate.rs | 100 +++++++++++++++++++++++ crates/domain/src/mutate/patch_user.rs | 13 +++ crates/domain/src/mutate_user.rs | 4 +- crates/domain/src/narinfo.rs | 23 ++---- crates/domain/src/upload/execute.rs | 8 +- crates/domain/src/upload/tests.rs | 4 +- 14 files changed, 275 insertions(+), 163 deletions(-) create mode 100644 crates/domain/src/mutate.rs create mode 100644 crates/domain/src/mutate/create.rs create mode 100644 crates/domain/src/mutate/delete_entry.rs create mode 100644 crates/domain/src/mutate/migrate.rs create mode 100644 crates/domain/src/mutate/patch_user.rs diff --git a/crates/domain/src/create.rs b/crates/domain/src/create.rs index 5b603f8e..fcbca7bc 100644 --- a/crates/domain/src/create.rs +++ b/crates/domain/src/create.rs @@ -14,16 +14,7 @@ impl DomainService { name: EntityName, visibility: Visibility, ) -> Result, CreateModelError> { - self - .cache_repo - .create_model(Cache { - id: RecordId::new(), - org, - name, - visibility, - }) - .await - .map(|c| c.id) + self.mutate.create_cache(org, name, visibility).await } /// Creates a [`Store`]. @@ -35,16 +26,9 @@ impl DomainService { config: StoreConfiguration, ) -> Result, CreateModelError> { self - .store_repo - .create_model(Store { - id: RecordId::new(), - org, - credentials, - config, - name, - }) + .mutate + .create_store(org, name, credentials, config) .await - .map(|s| s.id) } /// Creates an [`Org`]. @@ -52,13 +36,6 @@ impl DomainService { &self, name: EntityName, ) -> Result, CreateModelError> { - self - .org_repo - .create_model(Org { - id: RecordId::new(), - org_ident: models::OrgIdent::Named(name), - }) - .await - .map(|s| s.id) + self.mutate.create_org(name).await } } diff --git a/crates/domain/src/delete_entry.rs b/crates/domain/src/delete_entry.rs index 667a85fb..da9a2922 100644 --- a/crates/domain/src/delete_entry.rs +++ b/crates/domain/src/delete_entry.rs @@ -9,10 +9,6 @@ impl DomainService { &self, id: RecordId, ) -> Result>, DeleteModelError> { - self - .entry_repo - .delete_model(id) - .await - .map(|b| b.then_some(id)) + self.mutate.delete_entry(id).await } } diff --git a/crates/domain/src/download/tests.rs b/crates/domain/src/download/tests.rs index 41d52dcf..a87fd435 100644 --- a/crates/domain/src/download/tests.rs +++ b/crates/domain/src/download/tests.rs @@ -52,8 +52,8 @@ async fn test_download() { .expect("failed to execute upload"); let _entry = pds - .entry_repo - .fetch_model_by_id(resp.entry_id) + .meta() + .fetch_entry_by_id(resp.entry_id) .await .expect("failed to find entry") .expect("failed to find entry"); diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 8ec5dbe0..c4b8546f 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -4,6 +4,7 @@ mod create; mod delete_entry; pub mod download; mod migrate; +mod mutate; pub mod mutate_user; pub mod narinfo; pub mod upload; @@ -16,15 +17,13 @@ use meta_domain::MetaService; pub use models; use models::{Cache, Entry, Org, Store, User}; +use self::mutate::MutationService; + /// The domain service type. #[derive(Debug, Clone)] pub struct DomainService { - meta: MetaService, - org_repo: Database, - user_repo: Database, - store_repo: Database, - entry_repo: Database, - cache_repo: Database, + meta: MetaService, + mutate: MutationService, } impl DomainService { @@ -43,15 +42,15 @@ impl DomainService { entry_repo.clone(), cache_repo.clone(), ); + let mutate = MutationService::new( + org_repo.clone(), + user_repo.clone(), + store_repo.clone(), + entry_repo.clone(), + cache_repo.clone(), + ); - Self { - meta, - org_repo, - user_repo, - store_repo, - entry_repo, - cache_repo, - } + Self { meta, mutate } } /// Access the internal [`MetaService`]. diff --git a/crates/domain/src/migrate.rs b/crates/domain/src/migrate.rs index d760d88d..1889698e 100644 --- a/crates/domain/src/migrate.rs +++ b/crates/domain/src/migrate.rs @@ -1,100 +1,10 @@ -use std::{path::PathBuf, str::FromStr}; - -use miette::{Context, IntoDiagnostic, Result}; -use models::{ - Cache, LocalStorageCredentials, MemoryStorageCredentials, Org, OrgIdent, - Store, StoreConfiguration, User, - dvf::{ - EmailAddress, EntityName, HumanName, RecordId, StrictSlug, Visibility, - }, -}; +use miette::Result; use crate::DomainService; impl DomainService { /// Add test data to databases. pub async fn migrate_test_data(&self, ephemeral_storage: bool) -> Result<()> { - let user_id = RecordId::from_str("01JXGXV4R6VCZWQ2DAYDWR1VXD").unwrap(); - - let personal_org = self - .org_repo - .create_model(Org { - id: RecordId::from_str("01K202SRBQMRM29MMSTJTMSJVD").unwrap(), - org_ident: OrgIdent::UserOrg(user_id), - }) - .await - .into_diagnostic() - .context("failed to create org")?; - - let federation = self - .org_repo - .create_model(Org { - id: RecordId::from_str("01JXGXSB69BDHNFTSVG2EPW2M3").unwrap(), - org_ident: OrgIdent::Named(EntityName::new(StrictSlug::new( - "the-federation", - ))), - }) - .await - .into_diagnostic() - .context("failed to create org")?; - - let _user = self - .user_repo - .create_model(User { - id: user_id, - personal_org: personal_org.id, - orgs: vec![federation.id], - email: EmailAddress::try_new("jpicard@federation.gov") - .unwrap(), - name: HumanName::try_new("Jean-Luc Picard") - .expect("failed to create name"), - name_abbr: User::abbreviate_name( - HumanName::try_new("Jean-Luc Picard").expect("failed to create name"), - ), - auth: models::UserAuthCredentials::Password { - // hash for password `password` - password_hash: models::PasswordHash( - "$argon2id$v=19$m=16,t=2,\ - p=1$dGhpc2lzYXNhbHQ$dahcDJkLouoYfTwtXjg67Q" - .to_string(), - ), - }, - active_org_index: 1, - }) - .await - .into_diagnostic() - .context("failed to create user")?; - - let _albert_store = self - .store_repo - .create_model(Store { - id: RecordId::from_str("01JXGXVF0MVQNGRM565YHM20BC").unwrap(), - org: federation.id, - credentials: match ephemeral_storage { - true => models::StorageCredentials::Memory(MemoryStorageCredentials), - false => models::StorageCredentials::Local(LocalStorageCredentials( - PathBuf::from("/tmp/rambit-albert-store"), - )), - }, - config: StoreConfiguration {}, - name: EntityName::new(StrictSlug::new("albert")), - }) - .await - .into_diagnostic() - .context("failed to create store")?; - - let _aaron_cache = self - .cache_repo - .create_model(Cache { - id: RecordId::from_str("01JXGXVVE6J16590YJT3SP2P6M").unwrap(), - org: federation.id, - name: EntityName::new(StrictSlug::new("aaron")), - visibility: Visibility::Public, - }) - .await - .into_diagnostic() - .context("failed to create cache")?; - - Ok(()) + self.mutate.migrate_test_data(ephemeral_storage).await } } diff --git a/crates/domain/src/mutate.rs b/crates/domain/src/mutate.rs new file mode 100644 index 00000000..1c622860 --- /dev/null +++ b/crates/domain/src/mutate.rs @@ -0,0 +1,34 @@ +mod create; +mod delete_entry; +mod migrate; +mod patch_user; + +use db::Database; +use models::{Cache, Entry, Org, Store, User}; + +#[derive(Debug, Clone)] +pub struct MutationService { + org_repo: Database, + user_repo: Database, + store_repo: Database, + entry_repo: Database, + cache_repo: Database, +} + +impl MutationService { + pub fn new( + org_repo: Database, + user_repo: Database, + store_repo: Database, + entry_repo: Database, + cache_repo: Database, + ) -> Self { + Self { + org_repo, + user_repo, + store_repo, + entry_repo, + cache_repo, + } + } +} diff --git a/crates/domain/src/mutate/create.rs b/crates/domain/src/mutate/create.rs new file mode 100644 index 00000000..a0639f71 --- /dev/null +++ b/crates/domain/src/mutate/create.rs @@ -0,0 +1,72 @@ +use db::CreateModelError; +use models::{ + Cache, Entry, Org, StorageCredentials, Store, StoreConfiguration, + dvf::{EntityName, RecordId, Visibility}, +}; + +use super::MutationService; + +impl MutationService { + /// Creates a [`Cache`]. + pub async fn create_cache( + &self, + org: RecordId, + name: EntityName, + visibility: Visibility, + ) -> Result, CreateModelError> { + self + .cache_repo + .create_model(Cache { + id: RecordId::new(), + org, + name, + visibility, + }) + .await + .map(|c| c.id) + } + + /// Creates a [`Store`]. + pub async fn create_store( + &self, + org: RecordId, + name: EntityName, + credentials: StorageCredentials, + config: StoreConfiguration, + ) -> Result, CreateModelError> { + self + .store_repo + .create_model(Store { + id: RecordId::new(), + org, + credentials, + config, + name, + }) + .await + .map(|s| s.id) + } + + /// Creates an [`Org`]. + pub async fn create_org( + &self, + name: EntityName, + ) -> Result, CreateModelError> { + self + .org_repo + .create_model(Org { + id: RecordId::new(), + org_ident: models::OrgIdent::Named(name), + }) + .await + .map(|s| s.id) + } + + /// Creates an [`Entry`]. + pub async fn create_entry( + &self, + entry: Entry, + ) -> Result, CreateModelError> { + self.entry_repo.create_model(entry).await.map(|s| s.id) + } +} diff --git a/crates/domain/src/mutate/delete_entry.rs b/crates/domain/src/mutate/delete_entry.rs new file mode 100644 index 00000000..7fbc866d --- /dev/null +++ b/crates/domain/src/mutate/delete_entry.rs @@ -0,0 +1,18 @@ +use db::DeleteModelError; +use models::{Entry, dvf::RecordId}; + +use super::MutationService; + +impl MutationService { + /// Deletes an [`Entry`]. + pub async fn delete_entry( + &self, + id: RecordId, + ) -> Result>, DeleteModelError> { + self + .entry_repo + .delete_model(id) + .await + .map(|b| b.then_some(id)) + } +} diff --git a/crates/domain/src/mutate/migrate.rs b/crates/domain/src/mutate/migrate.rs new file mode 100644 index 00000000..17c6567e --- /dev/null +++ b/crates/domain/src/mutate/migrate.rs @@ -0,0 +1,100 @@ +use std::{path::PathBuf, str::FromStr}; + +use miette::{Context, IntoDiagnostic, Result}; +use models::{ + Cache, LocalStorageCredentials, MemoryStorageCredentials, Org, OrgIdent, + Store, StoreConfiguration, User, + dvf::{ + EmailAddress, EntityName, HumanName, RecordId, StrictSlug, Visibility, + }, +}; + +use super::MutationService; + +impl MutationService { + /// Add test data to databases. + pub async fn migrate_test_data(&self, ephemeral_storage: bool) -> Result<()> { + let user_id = RecordId::from_str("01JXGXV4R6VCZWQ2DAYDWR1VXD").unwrap(); + + let personal_org = self + .org_repo + .create_model(Org { + id: RecordId::from_str("01K202SRBQMRM29MMSTJTMSJVD").unwrap(), + org_ident: OrgIdent::UserOrg(user_id), + }) + .await + .into_diagnostic() + .context("failed to create org")?; + + let federation = self + .org_repo + .create_model(Org { + id: RecordId::from_str("01JXGXSB69BDHNFTSVG2EPW2M3").unwrap(), + org_ident: OrgIdent::Named(EntityName::new(StrictSlug::new( + "the-federation", + ))), + }) + .await + .into_diagnostic() + .context("failed to create org")?; + + let _user = self + .user_repo + .create_model(User { + id: user_id, + personal_org: personal_org.id, + orgs: vec![federation.id], + email: EmailAddress::try_new("jpicard@federation.gov") + .unwrap(), + name: HumanName::try_new("Jean-Luc Picard") + .expect("failed to create name"), + name_abbr: User::abbreviate_name( + HumanName::try_new("Jean-Luc Picard").expect("failed to create name"), + ), + auth: models::UserAuthCredentials::Password { + // hash for password `password` + password_hash: models::PasswordHash( + "$argon2id$v=19$m=16,t=2,\ + p=1$dGhpc2lzYXNhbHQ$dahcDJkLouoYfTwtXjg67Q" + .to_string(), + ), + }, + active_org_index: 1, + }) + .await + .into_diagnostic() + .context("failed to create user")?; + + let _albert_store = self + .store_repo + .create_model(Store { + id: RecordId::from_str("01JXGXVF0MVQNGRM565YHM20BC").unwrap(), + org: federation.id, + credentials: match ephemeral_storage { + true => models::StorageCredentials::Memory(MemoryStorageCredentials), + false => models::StorageCredentials::Local(LocalStorageCredentials( + PathBuf::from("/tmp/rambit-albert-store"), + )), + }, + config: StoreConfiguration {}, + name: EntityName::new(StrictSlug::new("albert")), + }) + .await + .into_diagnostic() + .context("failed to create store")?; + + let _aaron_cache = self + .cache_repo + .create_model(Cache { + id: RecordId::from_str("01JXGXVVE6J16590YJT3SP2P6M").unwrap(), + org: federation.id, + name: EntityName::new(StrictSlug::new("aaron")), + visibility: Visibility::Public, + }) + .await + .into_diagnostic() + .context("failed to create cache")?; + + Ok(()) + } +} diff --git a/crates/domain/src/mutate/patch_user.rs b/crates/domain/src/mutate/patch_user.rs new file mode 100644 index 00000000..36b767dc --- /dev/null +++ b/crates/domain/src/mutate/patch_user.rs @@ -0,0 +1,13 @@ +//! User mutation logic. + +use db::PatchModelError; +use models::User; + +use super::MutationService; + +impl MutationService { + /// Patches a [`User`]. + pub async fn patch_user(&self, user: User) -> Result { + self.user_repo.patch_model(user.id, user).await + } +} diff --git a/crates/domain/src/mutate_user.rs b/crates/domain/src/mutate_user.rs index 9e3d51bb..c517f98e 100644 --- a/crates/domain/src/mutate_user.rs +++ b/crates/domain/src/mutate_user.rs @@ -66,8 +66,8 @@ impl DomainService { }; self - .user_repo - .patch_model(user.id, new_user) + .mutate + .patch_user(new_user) .await .map_err(AddOrgToUserError::InternalPatchError)?; diff --git a/crates/domain/src/narinfo.rs b/crates/domain/src/narinfo.rs index ef45000c..42a16b61 100644 --- a/crates/domain/src/narinfo.rs +++ b/crates/domain/src/narinfo.rs @@ -2,9 +2,8 @@ use miette::{Context, IntoDiagnostic, miette}; use models::{ - CacheUniqueIndexSelector, Digest, Entry, EntryUniqueIndexSelector, Signature, - StorePath, User, - dvf::{EitherSlug, EntityName, RecordId, Visibility}, + Digest, Entry, Signature, StorePath, User, + dvf::{EntityName, RecordId, Visibility}, nix_compat::narinfo::{Flags, NarInfo}, }; @@ -95,11 +94,8 @@ impl DomainService { req: NarinfoRequest, ) -> Result { let cache = self - .cache_repo - .fetch_model_by_unique_index( - CacheUniqueIndexSelector::Name, - EitherSlug::Strict(req.cache_name.clone().into_inner()), - ) + .meta + .fetch_cache_by_name(req.cache_name.clone()) .await .into_diagnostic() .context("failed to search for cache") @@ -109,8 +105,8 @@ impl DomainService { let user = match req.auth { Some(user_id) => Some( self - .user_repo - .fetch_model_by_id(user_id) + .meta + .fetch_user_by_id(user_id) .await .into_diagnostic() .context("failed to find user") @@ -134,11 +130,8 @@ impl DomainService { } let entry = self - .entry_repo - .fetch_model_by_unique_index( - EntryUniqueIndexSelector::CacheIdAndEntryDigest, - Entry::unique_index_cache_id_and_entry_digest(cache.id, req.digest), - ) + .meta + .fetch_entry_by_cache_id_and_entry_digest(cache.id, req.digest) .await .context("failed to search for entry") .map_err(NarinfoError::InternalError)? diff --git a/crates/domain/src/upload/execute.rs b/crates/domain/src/upload/execute.rs index d169198d..202a0557 100644 --- a/crates/domain/src/upload/execute.rs +++ b/crates/domain/src/upload/execute.rs @@ -94,9 +94,9 @@ impl DomainService { let nar_authenticity_data = NarAuthenticityData::default(); // insert entry - let entry = self - .entry_repo - .create_model(Entry { + let entry_id = self + .mutate + .create_entry(Entry { id: entry_id, org: plan.org_id, caches: plan.caches.iter().map(Model::id).collect(), @@ -111,6 +111,6 @@ impl DomainService { .context("failed to create entry") .map_err(UploadExecutionError::InternalError)?; - Ok(UploadResponse { entry_id: entry.id }) + Ok(UploadResponse { entry_id }) } } diff --git a/crates/domain/src/upload/tests.rs b/crates/domain/src/upload/tests.rs index 41c3a6cb..b8718358 100644 --- a/crates/domain/src/upload/tests.rs +++ b/crates/domain/src/upload/tests.rs @@ -54,8 +54,8 @@ async fn test_upload() { .expect("failed to execute upload"); let _entry = pds - .entry_repo - .fetch_model_by_id(resp.entry_id) + .meta + .fetch_entry_by_id(resp.entry_id) .await .expect("failed to find entry") .expect("failed to find entry"); From 3cb055e91334631947f4daddaa86869faad66184 Mon Sep 17 00:00:00 2001 From: John Lewis Date: Sun, 19 Oct 2025 11:14:56 -0700 Subject: [PATCH 2/6] feat(domain+mutate-domain): isolate `mutate-domain` crate --- Cargo.lock | 11 +++++++++++ crates/domain/Cargo.toml | 1 + crates/domain/src/lib.rs | 5 ++--- crates/mutate-domain/Cargo.toml | 18 ++++++++++++++++++ .../src/mutate => mutate-domain/src}/create.rs | 0 .../src}/delete_entry.rs | 0 .../src/mutate.rs => mutate-domain/src/lib.rs} | 4 ++++ .../mutate => mutate-domain/src}/migrate.rs | 0 .../mutate => mutate-domain/src}/patch_user.rs | 0 9 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 crates/mutate-domain/Cargo.toml rename crates/{domain/src/mutate => mutate-domain/src}/create.rs (100%) rename crates/{domain/src/mutate => mutate-domain/src}/delete_entry.rs (100%) rename crates/{domain/src/mutate.rs => mutate-domain/src/lib.rs} (80%) rename crates/{domain/src/mutate => mutate-domain/src}/migrate.rs (100%) rename crates/{domain/src/mutate => mutate-domain/src}/patch_user.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 4bbd00f0..c974ed5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1354,6 +1354,7 @@ dependencies = [ "meta-domain", "miette", "models", + "mutate-domain", "owl", "serde", "storage", @@ -3066,6 +3067,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "mutate-domain" +version = "0.1.0" +dependencies = [ + "db", + "miette", + "models", + "thiserror 2.0.16", +] + [[package]] name = "next_tuple" version = "0.1.0" diff --git a/crates/domain/Cargo.toml b/crates/domain/Cargo.toml index f0db1760..fec4a3b1 100644 --- a/crates/domain/Cargo.toml +++ b/crates/domain/Cargo.toml @@ -9,6 +9,7 @@ publish = false [dependencies] meta-domain = { path = "../meta-domain" } models = { path = "../models" } +mutate-domain = { path = "../mutate-domain" } owl = { path = "../owl" } belt.workspace = true diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index c4b8546f..68bbbff7 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -4,7 +4,7 @@ mod create; mod delete_entry; pub mod download; mod migrate; -mod mutate; + pub mod mutate_user; pub mod narinfo; pub mod upload; @@ -16,8 +16,7 @@ use db::Database; use meta_domain::MetaService; pub use models; use models::{Cache, Entry, Org, Store, User}; - -use self::mutate::MutationService; +use mutate_domain::MutationService; /// The domain service type. #[derive(Debug, Clone)] diff --git a/crates/mutate-domain/Cargo.toml b/crates/mutate-domain/Cargo.toml new file mode 100644 index 00000000..dc018302 --- /dev/null +++ b/crates/mutate-domain/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "mutate-domain" +version = "0.1.0" + +edition = "2024" +license-file.workspace = true +publish = false + +[dependencies] +models = { path = "../models" } + +db.workspace = true + +miette.workspace = true +thiserror.workspace = true + +[lints] +workspace = true diff --git a/crates/domain/src/mutate/create.rs b/crates/mutate-domain/src/create.rs similarity index 100% rename from crates/domain/src/mutate/create.rs rename to crates/mutate-domain/src/create.rs diff --git a/crates/domain/src/mutate/delete_entry.rs b/crates/mutate-domain/src/delete_entry.rs similarity index 100% rename from crates/domain/src/mutate/delete_entry.rs rename to crates/mutate-domain/src/delete_entry.rs diff --git a/crates/domain/src/mutate.rs b/crates/mutate-domain/src/lib.rs similarity index 80% rename from crates/domain/src/mutate.rs rename to crates/mutate-domain/src/lib.rs index 1c622860..adf8a98e 100644 --- a/crates/domain/src/mutate.rs +++ b/crates/mutate-domain/src/lib.rs @@ -1,3 +1,5 @@ +//! Provides [`MutationService`] for mutation operations on models. + mod create; mod delete_entry; mod migrate; @@ -6,6 +8,7 @@ mod patch_user; use db::Database; use models::{Cache, Entry, Org, Store, User}; +/// Service for mutation operations on models. #[derive(Debug, Clone)] pub struct MutationService { org_repo: Database, @@ -16,6 +19,7 @@ pub struct MutationService { } impl MutationService { + /// Creates a new [`MutationService`]. pub fn new( org_repo: Database, user_repo: Database, diff --git a/crates/domain/src/mutate/migrate.rs b/crates/mutate-domain/src/migrate.rs similarity index 100% rename from crates/domain/src/mutate/migrate.rs rename to crates/mutate-domain/src/migrate.rs diff --git a/crates/domain/src/mutate/patch_user.rs b/crates/mutate-domain/src/patch_user.rs similarity index 100% rename from crates/domain/src/mutate/patch_user.rs rename to crates/mutate-domain/src/patch_user.rs From 12b8dfe3f27eb28f0269553c5c874baf09d74d28 Mon Sep 17 00:00:00 2001 From: John Lewis Date: Sun, 19 Oct 2025 11:21:34 -0700 Subject: [PATCH 3/6] chore(repo): update crate graph --- media/crate-graph.svg | 204 +++++++++++++++++++++++------------------- 1 file changed, 111 insertions(+), 93 deletions(-) diff --git a/media/crate-graph.svg b/media/crate-graph.svg index ed00f079..02b5c096 100644 --- a/media/crate-graph.svg +++ b/media/crate-graph.svg @@ -4,177 +4,195 @@ - + - + 0 - -auth-domain + +auth-domain 1 - -models + +models 0->1 - - + + 2 - -cli + +cli 2->1 - - + + 3 - -domain + +domain 3->1 - - + + 4 - -meta-domain + +meta-domain 3->4 - - + + 5 - -owl + +mutate-domain 3->5 - - - - - -4->1 - - - - - -5->1 - - + + 6 - -grid + +owl + + + +3->6 + + + + + +4->1 + + - + -6->0 - - +5->1 + + - + -6->3 - - +6->1 + + 7 - -site-app + +grid - + -6->7 - - +7->0 + + + + + +7->3 + + 8 - -tower-sessions-db-store + +site-app - - -6->8 - - - - + -7->0 - - +7->8 + + - - -7->1 - - + + +9 + +tower-sessions-db-store - + -7->3 - - +7->9 + + + + + +8->0 + + - + 8->1 - - + + - - -9 - -junk-cli + + +8->3 + + - + 9->1 - - + + 10 - -site-frontend - - - -10->7 - - + +junk-cli + + + +10->1 + + + + + +11 + +site-frontend + + + +11->8 + + From 316256914d7e920febf10ffe905ccec2fdba340d Mon Sep 17 00:00:00 2001 From: John Lewis Date: Sun, 19 Oct 2025 11:27:02 -0700 Subject: [PATCH 4/6] feat(auth-domain): migrate auth-domain to meta and mutate --- Cargo.lock | 3 +- crates/auth-domain/Cargo.toml | 3 +- crates/auth-domain/src/errors.rs | 43 +------ crates/auth-domain/src/lib.rs | 120 ++++++++------------ crates/auth-domain/src/tests.rs | 16 ++- crates/domain/src/create.rs | 8 +- crates/domain/src/lib.rs | 45 ++------ crates/grid/src/app_state.rs | 26 ++++- crates/meta-domain/src/fetch_user_by.rs | 20 ++++ crates/meta-domain/src/lib.rs | 12 ++ crates/mutate-domain/src/create.rs | 21 ++-- crates/mutate-domain/src/lib.rs | 14 +++ crates/mutate-domain/src/user_active_org.rs | 51 +++++++++ 13 files changed, 217 insertions(+), 165 deletions(-) create mode 100644 crates/meta-domain/src/fetch_user_by.rs create mode 100644 crates/mutate-domain/src/user_active_org.rs diff --git a/Cargo.lock b/Cargo.lock index c974ed5d..b7a218cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,9 +327,10 @@ dependencies = [ "argon2", "async-trait", "axum-login", - "db", + "meta-domain", "miette", "models", + "mutate-domain", "thiserror 2.0.16", "tokio", "tracing", diff --git a/crates/auth-domain/Cargo.toml b/crates/auth-domain/Cargo.toml index a5751731..8917ae29 100644 --- a/crates/auth-domain/Cargo.toml +++ b/crates/auth-domain/Cargo.toml @@ -9,7 +9,8 @@ publish = false [dependencies] models = { path = "../models" } -db.workspace = true +meta-domain = { path = "../meta-domain" } +mutate-domain = { path = "../mutate-domain" } miette.workspace = true thiserror.workspace = true diff --git a/crates/auth-domain/src/errors.rs b/crates/auth-domain/src/errors.rs index 7179ef8a..46ac6fa1 100644 --- a/crates/auth-domain/src/errors.rs +++ b/crates/auth-domain/src/errors.rs @@ -1,5 +1,4 @@ -use db::{FetchModelByIndexError, FetchModelError, PatchModelError}; -use models::{Org, User, dvf::EmailAddress, model::RecordId}; +use models::dvf::EmailAddress; /// An error that occurs during user creation. #[derive(Debug, thiserror::Error, miette::Diagnostic)] @@ -7,44 +6,12 @@ pub enum CreateUserError { /// Indicates that the user's email address is already in use. #[error("The email address is already in use: \"{0}\"")] EmailAlreadyUsed(EmailAddress), - /// Indicates than an error occurred while hashing the password. - #[error("Failed to hash password")] - PasswordHashing(miette::Report), - /// Indicates that an error occurred while creating the user. + /// Indicates that an internal error occurred. #[error("Failed to create the user")] - CreateError(miette::Report), - /// Indicates that an error occurred while fetching users by index. - #[error("Failed to fetch users by index")] - FetchByIndexError(#[from] FetchModelByIndexError), + InternalError(miette::Report), } /// An error that occurs during user authentication. #[derive(Debug, thiserror::Error, miette::Diagnostic)] -pub enum AuthenticationError { - /// Indicates that an error occurred while fetching users. - #[error("Failed to fetch user")] - FetchError(#[from] FetchModelError), - /// Indicates that an error occurred while fetching users by index. - #[error("Failed to fetch user by index")] - FetchByIndexError(#[from] FetchModelByIndexError), - /// Indicates than an error occurred while hashing the password. - #[error("Failed to hash password")] - PasswordHashing(miette::Report), -} - -/// An error that occurs while updating a user's active org. -#[derive(Debug, thiserror::Error, miette::Diagnostic)] -pub enum UpdateActiveOrgError { - /// Indicates that an error occurred while fetching the user. - #[error("Failed to fetch user")] - FetchError(#[from] FetchModelError), - /// Indicates that the user does not exist. - #[error("Failed to find user: {0}")] - MissingUser(RecordId), - /// Indicates that the org supplied could not be switched to. - #[error("Failed to switch to org: {0}")] - InvalidOrg(RecordId), - /// Indicates that an error occurred while patching the user record. - #[error("Failed to patch user")] - PatchError(#[from] PatchModelError), -} +#[error("Internal error: {0}")] +pub struct AuthenticationError(pub miette::Report); diff --git a/crates/auth-domain/src/lib.rs b/crates/auth-domain/src/lib.rs index 231a9148..c2351ec6 100644 --- a/crates/auth-domain/src/lib.rs +++ b/crates/auth-domain/src/lib.rs @@ -7,14 +7,14 @@ mod tests; use axum_login::AuthUser as AxumLoginAuthUser; pub use axum_login::AuthnBackend; -use db::{Database, FetchModelByIndexError, FetchModelError, kv::LaxSlug}; -use miette::{IntoDiagnostic, miette}; +use miette::{Context, IntoDiagnostic, miette}; use models::{ AuthUser, Org, OrgIdent, User, UserAuthCredentials, - UserSubmittedAuthCredentials, UserUniqueIndexSelector, - dvf::{EitherSlug, EmailAddress, HumanName}, + UserSubmittedAuthCredentials, + dvf::{EmailAddress, HumanName}, model::RecordId, }; +pub use mutate_domain::UpdateActiveOrgError; pub use self::errors::*; @@ -24,70 +24,29 @@ pub type AuthSession = axum_login::AuthSession; /// A dynamic [`AuthDomainService`] trait object. #[derive(Clone, Debug)] pub struct AuthDomainService { - org_repo: Database, - user_repo: Database, + meta: meta_domain::MetaService, + mutate: mutate_domain::MutationService, } impl AuthDomainService { /// Creates a new [`AuthDomainService`]. #[must_use] - pub fn new(org_repo: Database, user_repo: Database) -> Self { - Self { - org_repo, - user_repo, - } + pub fn new( + meta: meta_domain::MetaService, + mutate: mutate_domain::MutationService, + ) -> Self { + Self { meta, mutate } } } impl AuthDomainService { - /// Fetch a [`User`] by ID. - async fn fetch_user_by_id( - &self, - id: RecordId, - ) -> Result, FetchModelError> { - self.user_repo.fetch_model_by_id(id).await - } - - /// Fetch a [`User`] by [`EmailAddress`](EmailAddress). - async fn fetch_user_by_email( - &self, - email: EmailAddress, - ) -> Result, FetchModelByIndexError> { - self - .user_repo - .fetch_model_by_unique_index( - UserUniqueIndexSelector::Email, - EitherSlug::Lax(LaxSlug::new(email.as_ref())), - ) - .await - } - - /// Switch a [`User`]'s active org. + /// Switches the active org of a [`User`]. pub async fn switch_active_org( &self, user: RecordId, new_active_org: RecordId, - ) -> Result, errors::UpdateActiveOrgError> { - let user = self - .user_repo - .fetch_model_by_id(user) - .await? - .ok_or(errors::UpdateActiveOrgError::MissingUser(user))?; - - let new_index = user - .iter_orgs() - .position(|o| o == new_active_org) - .ok_or(errors::UpdateActiveOrgError::InvalidOrg(new_active_org))?; - - self - .user_repo - .patch_model(user.id, User { - active_org_index: new_index as _, - ..user - }) - .await?; - - Ok(new_active_org) + ) -> Result, UpdateActiveOrgError> { + self.mutate.switch_active_org(user, new_active_org).await } /// Sign up a [`User`]. @@ -99,7 +58,15 @@ impl AuthDomainService { ) -> Result { use argon2::PasswordHasher; - if self.fetch_user_by_email(email.clone()).await?.is_some() { + if self + .meta + .fetch_user_by_email(email.clone()) + .await + .into_diagnostic() + .context("failed to check for conflicting user by email") + .map_err(CreateUserError::InternalError)? + .is_some() + { return Err(errors::CreateUserError::EmailAlreadyUsed(email)); } @@ -113,8 +80,8 @@ impl AuthDomainService { argon .hash_password(password.as_bytes(), &salt) .map_err(|e| { - errors::CreateUserError::PasswordHashing(miette!( - "failed to hash password: {e}" + CreateUserError::InternalError(miette!( + "failed to parse password hash: {e}" )) })? .to_string(), @@ -143,18 +110,22 @@ impl AuthDomainService { }; self - .org_repo - .create_model(org) + .mutate + .create_org(org) .await .into_diagnostic() - .map_err(errors::CreateUserError::CreateError)?; + .context("failed to create personal org for user") + .map_err(errors::CreateUserError::InternalError)?; self - .user_repo - .create_model(user) + .mutate + .create_user(user.clone()) .await .into_diagnostic() - .map_err(errors::CreateUserError::CreateError) + .context("failed to create user") + .map_err(errors::CreateUserError::InternalError)?; + + Ok(user) } /// Authenticate a [`User`]. @@ -162,10 +133,17 @@ impl AuthDomainService { &self, email: EmailAddress, creds: UserSubmittedAuthCredentials, - ) -> Result, errors::AuthenticationError> { + ) -> Result, AuthenticationError> { use argon2::PasswordVerifier; - let Some(user) = self.fetch_user_by_email(email).await? else { + let Some(user) = self + .meta + .fetch_user_by_email(email) + .await + .into_diagnostic() + .context("failed to fetch user by email") + .map_err(AuthenticationError)? + else { return Ok(None); }; @@ -176,9 +154,7 @@ impl AuthDomainService { ) => { let password_hash = argon2::PasswordHash::new(&password_hash.0) .map_err(|e| { - errors::AuthenticationError::PasswordHashing(miette!( - "failed to parse password hash: {e}" - )) + AuthenticationError(miette!("failed to parse password hash: {e}")) })?; let argon = argon2::Argon2::default(); @@ -189,7 +165,7 @@ impl AuthDomainService { Err(e) => Err(e), }) .map_err(|e| { - errors::AuthenticationError::PasswordHashing(miette!( + AuthenticationError(miette!( "failed to verify password against hash: {e}" )) })?; @@ -220,9 +196,11 @@ impl AuthnBackend for AuthDomainService { id: &::Id, ) -> Result, Self::Error> { self + .meta .fetch_user_by_id(*id) .await + .into_diagnostic() .map(|u| u.map(Into::into)) - .map_err(Into::into) + .map_err(AuthenticationError) } } diff --git a/crates/auth-domain/src/tests.rs b/crates/auth-domain/src/tests.rs index 2e50a1cd..24107624 100644 --- a/crates/auth-domain/src/tests.rs +++ b/crates/auth-domain/src/tests.rs @@ -1,12 +1,15 @@ +use meta_domain::MetaService; use models::dvf::{EmailAddress, HumanName}; +use mutate_domain::MutationService; use super::*; #[tokio::test] async fn test_user_signup() { - let org_repo = Database::new_mock(); - let user_repo = Database::new_mock(); - let service = AuthDomainService::new(org_repo, user_repo); + let service = AuthDomainService::new( + MetaService::new_mock(), + MutationService::new_mock(), + ); let name = HumanName::try_new("Test User 1").unwrap(); let email = EmailAddress::try_new("test@example.com").unwrap(); @@ -30,9 +33,10 @@ async fn test_user_signup() { #[tokio::test] async fn test_user_authenticate() { - let org_repo = Database::new_mock(); - let user_repo = Database::new_mock(); - let service = AuthDomainService::new(org_repo, user_repo); + let service = AuthDomainService::new( + MetaService::new_mock(), + MutationService::new_mock(), + ); let name = HumanName::try_new("Test User 1").unwrap(); let email = EmailAddress::try_new("test@example.com").unwrap(); diff --git a/crates/domain/src/create.rs b/crates/domain/src/create.rs index fcbca7bc..7a02b9f5 100644 --- a/crates/domain/src/create.rs +++ b/crates/domain/src/create.rs @@ -36,6 +36,12 @@ impl DomainService { &self, name: EntityName, ) -> Result, CreateModelError> { - self.mutate.create_org(name).await + self + .mutate + .create_org(Org { + id: RecordId::new(), + org_ident: models::OrgIdent::Named(name), + }) + .await } } diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 68bbbff7..dc19dc11 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -12,10 +12,10 @@ pub mod upload; pub use belt; pub use bytes; pub use db; -use db::Database; +pub use meta_domain; use meta_domain::MetaService; pub use models; -use models::{Cache, Entry, Org, Store, User}; +pub use mutate_domain; use mutate_domain::MutationService; /// The domain service type. @@ -27,50 +27,29 @@ pub struct DomainService { impl DomainService { /// Create a new [`DomainService`]. - pub fn new( - org_repo: Database, - user_repo: Database, - store_repo: Database, - entry_repo: Database, - cache_repo: Database, - ) -> Self { - let meta = MetaService::new( - org_repo.clone(), - user_repo.clone(), - store_repo.clone(), - entry_repo.clone(), - cache_repo.clone(), - ); - let mutate = MutationService::new( - org_repo.clone(), - user_repo.clone(), - store_repo.clone(), - entry_repo.clone(), - cache_repo.clone(), - ); - + pub fn new(meta: MetaService, mutate: MutationService) -> Self { Self { meta, mutate } } + /// Create a mocked-up [`DomainService`]. + pub fn new_mock() -> Self { + Self { + meta: MetaService::new_mock(), + mutate: MutationService::new_mock(), + } + } + /// Access the internal [`MetaService`]. pub fn meta(&self) -> &MetaService { &self.meta } } #[cfg(test)] mod tests { - use db::Database; - use crate::DomainService; impl DomainService { pub(crate) async fn mock_domain() -> DomainService { - let pds = DomainService::new( - Database::new_mock(), - Database::new_mock(), - Database::new_mock(), - Database::new_mock(), - Database::new_mock(), - ); + let pds = DomainService::new_mock(); pds .migrate_test_data(true) diff --git a/crates/grid/src/app_state.rs b/crates/grid/src/app_state.rs index aa42be98..fe169363 100644 --- a/crates/grid/src/app_state.rs +++ b/crates/grid/src/app_state.rs @@ -3,7 +3,10 @@ use std::sync::Arc; use auth_domain::AuthDomainService; use axum::extract::FromRef; -use domain::{DomainService, db::Database}; +use domain::{ + DomainService, db::Database, meta_domain::MetaService, + mutate_domain::MutationService, +}; use leptos::config::LeptosOptions; use miette::{Context, IntoDiagnostic, Result}; use tower_sessions_db_store::DatabaseStore as DatabaseSessionStore; @@ -64,9 +67,24 @@ impl AppState { ) }; - let auth_domain = AuthDomainService::new(org_db.clone(), user_db.clone()); - let domain = - DomainService::new(org_db, user_db, store_db, entry_db, cache_db); + let meta_domain = MetaService::new( + org_db.clone(), + user_db.clone(), + store_db.clone(), + entry_db.clone(), + cache_db.clone(), + ); + let mutate_domain = MutationService::new( + org_db.clone(), + user_db.clone(), + store_db.clone(), + entry_db.clone(), + cache_db, + ); + + let auth_domain = + AuthDomainService::new(meta_domain.clone(), mutate_domain.clone()); + let domain = DomainService::new(meta_domain, mutate_domain); let session_store = DatabaseSessionStore::new(session_db); let leptos_conf = leptos::prelude::get_configuration(None) diff --git a/crates/meta-domain/src/fetch_user_by.rs b/crates/meta-domain/src/fetch_user_by.rs new file mode 100644 index 00000000..78958c00 --- /dev/null +++ b/crates/meta-domain/src/fetch_user_by.rs @@ -0,0 +1,20 @@ +use db::{FetchModelByIndexError, kv::LaxSlug}; +use models::{User, UserUniqueIndexSelector, dvf::EmailAddress}; + +use crate::MetaService; + +impl MetaService { + /// Fetch a [`User`] by [`EmailAddress`](EmailAddress). + pub async fn fetch_user_by_email( + &self, + email: EmailAddress, + ) -> Result, FetchModelByIndexError> { + self + .user_repo + .fetch_model_by_unique_index( + UserUniqueIndexSelector::Email, + LaxSlug::new(email.as_ref()).into(), + ) + .await + } +} diff --git a/crates/meta-domain/src/lib.rs b/crates/meta-domain/src/lib.rs index 4ab0a7a9..734d7ead 100644 --- a/crates/meta-domain/src/lib.rs +++ b/crates/meta-domain/src/lib.rs @@ -5,6 +5,7 @@ mod fetch_by_id; mod fetch_by_name; mod fetch_by_org; mod fetch_entry_by; +mod fetch_user_by; mod search_stores_by_user; use db::Database; @@ -39,4 +40,15 @@ impl MetaService { cache_repo, } } + + /// Creates a mocked-up [`MetaService`]. + pub fn new_mock() -> Self { + Self { + org_repo: Database::new_mock(), + user_repo: Database::new_mock(), + store_repo: Database::new_mock(), + entry_repo: Database::new_mock(), + cache_repo: Database::new_mock(), + } + } } diff --git a/crates/mutate-domain/src/create.rs b/crates/mutate-domain/src/create.rs index a0639f71..68cce708 100644 --- a/crates/mutate-domain/src/create.rs +++ b/crates/mutate-domain/src/create.rs @@ -1,6 +1,6 @@ use db::CreateModelError; use models::{ - Cache, Entry, Org, StorageCredentials, Store, StoreConfiguration, + Cache, Entry, Org, StorageCredentials, Store, StoreConfiguration, User, dvf::{EntityName, RecordId, Visibility}, }; @@ -50,16 +50,17 @@ impl MutationService { /// Creates an [`Org`]. pub async fn create_org( &self, - name: EntityName, + org: Org, ) -> Result, CreateModelError> { - self - .org_repo - .create_model(Org { - id: RecordId::new(), - org_ident: models::OrgIdent::Named(name), - }) - .await - .map(|s| s.id) + self.org_repo.create_model(org).await.map(|s| s.id) + } + + /// Creates a [`User`]. + pub async fn create_user( + &self, + user: User, + ) -> Result, CreateModelError> { + self.user_repo.create_model(user).await.map(|u| u.id) } /// Creates an [`Entry`]. diff --git a/crates/mutate-domain/src/lib.rs b/crates/mutate-domain/src/lib.rs index adf8a98e..73bdf923 100644 --- a/crates/mutate-domain/src/lib.rs +++ b/crates/mutate-domain/src/lib.rs @@ -4,10 +4,13 @@ mod create; mod delete_entry; mod migrate; mod patch_user; +mod user_active_org; use db::Database; use models::{Cache, Entry, Org, Store, User}; +pub use self::user_active_org::UpdateActiveOrgError; + /// Service for mutation operations on models. #[derive(Debug, Clone)] pub struct MutationService { @@ -35,4 +38,15 @@ impl MutationService { cache_repo, } } + + /// Creates a mocked-up [`MutationService`]. + pub fn new_mock() -> Self { + Self { + org_repo: Database::new_mock(), + user_repo: Database::new_mock(), + store_repo: Database::new_mock(), + entry_repo: Database::new_mock(), + cache_repo: Database::new_mock(), + } + } } diff --git a/crates/mutate-domain/src/user_active_org.rs b/crates/mutate-domain/src/user_active_org.rs new file mode 100644 index 00000000..6b668383 --- /dev/null +++ b/crates/mutate-domain/src/user_active_org.rs @@ -0,0 +1,51 @@ +use db::{FetchModelError, PatchModelError}; +use models::{Org, User, dvf::RecordId}; + +use crate::MutationService; + +/// An error that occurs while updating a user's active org. +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum UpdateActiveOrgError { + /// Indicates that an error occurred while fetching the user. + #[error("Failed to fetch user")] + FetchError(#[from] FetchModelError), + /// Indicates that the user does not exist. + #[error("Failed to find user: {0}")] + MissingUser(RecordId), + /// Indicates that the org supplied could not be switched to. + #[error("Failed to switch to org: {0}")] + InvalidOrg(RecordId), + /// Indicates that an error occurred while patching the user record. + #[error("Failed to patch user")] + PatchError(#[from] PatchModelError), +} + +impl MutationService { + /// Switch a [`User`]'s active org. + pub async fn switch_active_org( + &self, + user: RecordId, + new_active_org: RecordId, + ) -> Result, UpdateActiveOrgError> { + let user = self + .user_repo + .fetch_model_by_id(user) + .await? + .ok_or(UpdateActiveOrgError::MissingUser(user))?; + + let new_index = user + .iter_orgs() + .position(|o| o == new_active_org) + .ok_or(UpdateActiveOrgError::InvalidOrg(new_active_org))?; + + self + .user_repo + .patch_model(user.id, User { + active_org_index: new_index as _, + ..user + }) + .await?; + + Ok(new_active_org) + } +} From 279b2d090b0ef0bf125b69160a801390f16e0812 Mon Sep 17 00:00:00 2001 From: John Lewis Date: Sun, 19 Oct 2025 12:07:28 -0700 Subject: [PATCH 5/6] chore(repo): update crate graph --- media/crate-graph.svg | 230 ++++++++++++++++++++++-------------------- 1 file changed, 121 insertions(+), 109 deletions(-) diff --git a/media/crate-graph.svg b/media/crate-graph.svg index 02b5c096..1dbbe27c 100644 --- a/media/crate-graph.svg +++ b/media/crate-graph.svg @@ -4,195 +4,207 @@ - + - + 0 - -auth-domain + +auth-domain 1 - -models + +meta-domain 0->1 - - + + 2 - -cli + +models - + -2->1 - - +0->2 + + 3 - -domain + +mutate-domain - + + +0->3 + + + + -3->1 - - +1->2 + + + + + +3->2 + + 4 - -meta-domain + +cli - - -3->4 - - + + +4->2 + + 5 - -mutate-domain + +domain - - -3->5 - - + + +5->1 + + + + + +5->2 + + + + + +5->3 + + 6 - -owl - - - -3->6 - - + +owl - - -4->1 - - - - - -5->1 - - + + +5->6 + + - - -6->1 - - + + +6->2 + + 7 - -grid + +grid - + 7->0 - - + + - - -7->3 - - + + +7->5 + + 8 - -site-app + +site-app - + 7->8 - - + + 9 - -tower-sessions-db-store + +tower-sessions-db-store - + 7->9 - - + + - -8->0 - - - - -8->1 - - +8->0 + + - - -8->3 - - + + +8->2 + + - + -9->1 - - +8->5 + + + + + +9->2 + + 10 - -junk-cli + +junk-cli - - -10->1 - - + + +10->2 + + 11 - -site-frontend + +site-frontend - + 11->8 - - + + From 080f04794e16becbf8b41dbc68ea2ac47d00420c Mon Sep 17 00:00:00 2001 From: John Lewis Date: Sun, 19 Oct 2025 12:10:04 -0700 Subject: [PATCH 6/6] chore(docs): fix docs errors --- crates/meta-domain/src/fetch_user_by.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/meta-domain/src/fetch_user_by.rs b/crates/meta-domain/src/fetch_user_by.rs index 78958c00..905de03d 100644 --- a/crates/meta-domain/src/fetch_user_by.rs +++ b/crates/meta-domain/src/fetch_user_by.rs @@ -4,7 +4,7 @@ use models::{User, UserUniqueIndexSelector, dvf::EmailAddress}; use crate::MetaService; impl MetaService { - /// Fetch a [`User`] by [`EmailAddress`](EmailAddress). + /// Fetch a [`User`] by [`EmailAddress`]. pub async fn fetch_user_by_email( &self, email: EmailAddress,