es-entity 0.12.17

Event Sourcing Entity Framework
Documentation
#![cfg(feature = "instrument")]

mod entities;
mod helpers;

use std::sync::{Arc, Mutex};

use entities::order::*;
use es_entity::*;
use helpers::init_pool;
use sqlx::PgPool;
use tracing_subscriber::layer::SubscriberExt;

#[derive(EsRepo, Debug)]
#[es_repo(entity = "Order", delete = "soft")]
pub struct Orders {
    pool: PgPool,

    #[es_repo(nested)]
    items: OrderItems,
}

impl Orders {
    pub fn new(pool: PgPool) -> Self {
        Self {
            pool: pool.clone(),
            items: OrderItems::new(pool),
        }
    }
}

#[derive(EsRepo, Debug)]
#[es_repo(
    entity = "OrderItem",
    delete = "soft",
    columns(order_id(ty = "OrderId", update(persist = false), parent))
)]
pub struct OrderItems {
    pool: PgPool,
}

impl OrderItems {
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }
}

#[derive(Clone, Default)]
struct QueryEventCount(Arc<Mutex<usize>>);

impl QueryEventCount {
    fn get(&self) -> usize {
        *self.0.lock().unwrap()
    }
}

impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for QueryEventCount {
    fn on_event(
        &self,
        event: &tracing::Event<'_>,
        _ctx: tracing_subscriber::layer::Context<'_, S>,
    ) {
        if event.metadata().target() == "sqlx::query" {
            *self.0.lock().unwrap() += 1;
        }
    }
}

async fn seed_order(orders: &Orders, item_names: &[&str]) -> anyhow::Result<OrderId> {
    let order_id = OrderId::new();
    let mut order = orders
        .create(NewOrderBuilder::default().id(order_id).build().unwrap())
        .await?;
    for name in item_names {
        order.add_item(
            NewOrderItemBuilder::default()
                .id(OrderItemId::new())
                .order_id(order_id)
                .product_name(*name)
                .quantity(1)
                .price(1.0)
                .build()
                .unwrap(),
        );
    }
    orders.update(&mut order).await?;
    Ok(order_id)
}

#[tokio::test]
async fn nested_find_by_id_is_one_statement_no_transaction() -> anyhow::Result<()> {
    let pool = init_pool().await?;
    let orders = Orders::new(pool);

    let order_id = seed_order(&orders, &["Laptop", "Mouse", "Keyboard"]).await?;

    let counter = QueryEventCount::default();
    let subscriber = tracing_subscriber::registry().with(counter.clone());
    let _guard = tracing::subscriber::set_default(subscriber);

    let order = orders.find_by_id(order_id).await?;

    assert_eq!(
        counter.get(),
        1,
        "a nested find_by_id must issue exactly one SQL statement for the whole tree"
    );
    assert_eq!(order.n_items(), 3);

    Ok(())
}

#[tokio::test]
async fn nested_find_all_is_one_statement() -> anyhow::Result<()> {
    let pool = init_pool().await?;
    let orders = Orders::new(pool);

    let mut ids = Vec::new();
    for i in 0..5 {
        ids.push(seed_order(&orders, &[&format!("item-{i}-a"), &format!("item-{i}-b")]).await?);
    }

    let counter = QueryEventCount::default();
    let subscriber = tracing_subscriber::registry().with(counter.clone());
    let _guard = tracing::subscriber::set_default(subscriber);

    let loaded = orders.find_all::<Order>(&ids).await?;

    assert_eq!(
        counter.get(),
        1,
        "find_all across 5 parents with children must still be one statement"
    );
    assert_eq!(loaded.len(), 5);
    for order in loaded.values() {
        assert_eq!(order.n_items(), 2);
    }

    Ok(())
}

#[tokio::test]
async fn nested_list_by_id_is_one_statement() -> anyhow::Result<()> {
    let pool = init_pool().await?;
    let orders = Orders::new(pool);

    for i in 0..3 {
        seed_order(&orders, &[&format!("item-{i}")]).await?;
    }

    let counter = QueryEventCount::default();
    let subscriber = tracing_subscriber::registry().with(counter.clone());
    let _guard = tracing::subscriber::set_default(subscriber);

    let page = orders
        .list_by_id(
            es_entity::PaginatedQueryArgs {
                first: 10,
                after: None,
            },
            es_entity::ListDirection::Ascending,
        )
        .await?;

    assert_eq!(
        counter.get(),
        1,
        "list_by_id must issue exactly one SQL statement regardless of page size"
    );
    assert!(page.entities.len() >= 3);

    Ok(())
}