mod helpers;
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use es_entity::*;
es_entity::entity_id! { AccountId, AccountHolderId }
#[derive(EsEvent, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[es_event(id = "AccountHolderId")]
pub enum AccountHolderEvent {
Initialized {
id: AccountHolderId,
account_id: AccountId,
name: Forgettable<String>,
email: Forgettable<String>,
},
}
#[derive(EsEntity, Builder)]
#[builder(pattern = "owned", build_fn(error = "EntityHydrationError"))]
pub struct AccountHolder {
pub id: AccountHolderId,
pub account_id: AccountId,
pub name: Forgettable<String>,
pub email: Forgettable<String>,
events: EntityEvents<AccountHolderEvent>,
}
impl TryFromEvents<AccountHolderEvent> for AccountHolder {
fn try_from_events(
events: EntityEvents<AccountHolderEvent>,
) -> Result<Self, EntityHydrationError> {
let mut builder = AccountHolderBuilder::default();
for event in events.iter_all() {
match event {
AccountHolderEvent::Initialized {
id,
account_id,
name,
email,
} => {
builder = builder
.id(*id)
.account_id(*account_id)
.name(name.clone())
.email(email.clone());
}
}
}
builder.events(events).build()
}
}
#[derive(Debug, Builder)]
pub struct NewAccountHolder {
pub id: AccountHolderId,
pub account_id: AccountId,
#[builder(setter(into))]
pub name: String,
#[builder(setter(into))]
pub email: String,
}
impl NewAccountHolder {
pub fn builder() -> NewAccountHolderBuilder {
NewAccountHolderBuilder::default()
}
}
impl IntoEvents<AccountHolderEvent> for NewAccountHolder {
fn into_events(self) -> EntityEvents<AccountHolderEvent> {
EntityEvents::init(
self.id,
[AccountHolderEvent::Initialized {
id: self.id,
account_id: self.account_id,
name: Forgettable::new(self.name),
email: Forgettable::new(self.email),
}],
)
}
}
#[derive(EsEvent, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[es_event(id = "AccountId")]
pub enum AccountEvent {
Initialized { id: AccountId },
}
#[derive(EsEntity, Builder)]
#[builder(pattern = "owned", build_fn(error = "EntityHydrationError"))]
pub struct Account {
pub id: AccountId,
events: EntityEvents<AccountEvent>,
#[es_entity(nested)]
#[builder(default)]
holders: Nested<AccountHolder>,
}
impl Account {
pub fn add_holder(&mut self, holder: NewAccountHolder) {
self.holders.add_new(holder);
}
pub fn n_holders(&self) -> usize {
self.holders.len_persisted()
}
}
impl TryFromEvents<AccountEvent> for Account {
fn try_from_events(events: EntityEvents<AccountEvent>) -> Result<Self, EntityHydrationError> {
let mut builder = AccountBuilder::default();
for event in events.iter_all() {
match event {
AccountEvent::Initialized { id } => builder = builder.id(*id),
}
}
builder.events(events).build()
}
}
#[derive(Debug, Builder)]
pub struct NewAccount {
pub id: AccountId,
}
impl NewAccount {
pub fn builder() -> NewAccountBuilder {
NewAccountBuilder::default()
}
}
impl IntoEvents<AccountEvent> for NewAccount {
fn into_events(self) -> EntityEvents<AccountEvent> {
EntityEvents::init(self.id, [AccountEvent::Initialized { id: self.id }])
}
}
#[derive(EsRepo, Debug)]
#[es_repo(entity = "Account", delete = "soft")]
pub struct Accounts {
pool: PgPool,
#[es_repo(nested)]
holders: AccountHolders,
}
impl Accounts {
pub fn new(pool: PgPool) -> Self {
Self {
pool: pool.clone(),
holders: AccountHolders::new(pool),
}
}
}
#[derive(EsRepo, Debug)]
#[es_repo(
entity = "AccountHolder",
forgettable,
delete = "soft",
columns(
account_id(ty = "AccountId", update(persist = false), parent),
email(ty = "Forgettable<String>", list_by)
)
)]
pub struct AccountHolders {
pool: PgPool,
}
impl AccountHolders {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
#[tokio::test]
async fn cascade_delete_scrubs_forgettable_nested_children() -> anyhow::Result<()> {
let pool = helpers::init_pool().await?;
let accounts = Accounts::new(pool.clone());
let account_id = AccountId::new();
let holder_id = AccountHolderId::new();
let email = format!("holder-{holder_id}@example.com");
let mut account = accounts
.create(NewAccount::builder().id(account_id).build().unwrap())
.await?;
account.add_holder(
NewAccountHolder::builder()
.id(holder_id)
.account_id(account_id)
.name("Grace Hopper")
.email(email.clone())
.build()
.unwrap(),
);
accounts.update(&mut account).await?;
let loaded = accounts.find_by_id(account_id).await?;
assert_eq!(loaded.n_holders(), 1);
let row = sqlx::query!(
"SELECT email FROM account_holders WHERE id = $1",
holder_id as AccountHolderId,
)
.fetch_one(&pool)
.await?;
assert_eq!(row.email.as_deref(), Some(email.as_str()));
let payloads = sqlx::query!(
"SELECT COUNT(*) AS count FROM account_holders_forgettable_payloads WHERE entity_id = $1",
holder_id as AccountHolderId,
)
.fetch_one(&pool)
.await?;
assert_eq!(payloads.count, Some(1));
let account = accounts.find_by_id(account_id).await?;
accounts.delete(account).await?;
let row = sqlx::query!(
"SELECT deleted, email FROM account_holders WHERE id = $1",
holder_id as AccountHolderId,
)
.fetch_one(&pool)
.await?;
assert!(row.deleted);
assert_eq!(row.email, None);
let payloads = sqlx::query!(
"SELECT COUNT(*) AS count FROM account_holders_forgettable_payloads WHERE entity_id = $1",
holder_id as AccountHolderId,
)
.fetch_one(&pool)
.await?;
assert_eq!(payloads.count, Some(0));
Ok(())
}