orlok 0.3.0

A database toolkit with generic implementations of the Repository pattern.
Documentation
use std::collections::HashMap;

use sqlx::postgres::PgRow;
use sqlx::{PgPool, Row};
use uuid::Uuid;

use orlok::pg::{PgDb, PgRepo, Value};
use orlok::{Repo, F};

#[derive(Debug, PartialEq)]
struct User {
    id: Uuid,
    name: String,
    emails: Vec<String>,
}

impl User {
    fn new(name: &str, emails: Vec<String>) -> Self {
        Self {
            id: Uuid::new_v4(),
            name: name.to_string(),
            emails,
        }
    }
}

fn dump_user(entity: &User) -> HashMap<String, Value> {
    HashMap::from([
        ("id".to_string(), entity.id.into()),
        ("name".to_string(), entity.name.clone().into()),
    ])
}

fn load_user(row: &PgRow) -> User {
    User {
        id: row.get("id"),
        name: row.get("name"),
        emails: row.get("emails"),
    }
}

fn users_repo() -> PgRepo<User> {
    PgRepo::new("users_with_emails", dump_user, load_user)
        .query(
            "select u.id, u.name, array_agg(emails.email) as emails
            from users_with_emails as u
            left join emails
            on emails.user_id = u.id
            group by u.id, u.name",
        )
        .after_add(|u| {
            u.emails
                .iter()
                .map(|e| {
                    sqlx::query("insert into emails (id, user_id, email) values ($1, $2, $3)")
                        .bind(Uuid::new_v4())
                        .bind(u.id)
                        .bind(e)
                })
                .collect()
        })
        .after_update(|u| {
            let mut queries = vec![sqlx::query("delete from emails where user_id = $1").bind(u.id)];
            queries.extend(u.emails.iter().map(|e| {
                sqlx::query("insert into emails (id, user_id, email) values ($1, $2, $3)")
                    .bind(Uuid::new_v4())
                    .bind(u.id)
                    .bind(e)
            }));
            queries
        })
}

pub async fn db<'a>() -> PgDb<'a> {
    let pool = PgPool::connect("postgresql://orlok:orlok@localhost/orlok")
        .await
        .unwrap();

    sqlx::query(
        "create table if not exists users_with_emails (
            id uuid primary key,
            name text
        )",
    )
    .execute(&pool)
    .await
    .unwrap();

    sqlx::query(
        "create table if not exists emails (
            id uuid primary key,
            user_id uuid references users_with_emails(id) on delete cascade,
            email text
        )",
    )
    .execute(&pool)
    .await
    .unwrap();

    sqlx::query("delete from users_with_emails")
        .execute(&pool)
        .await
        .unwrap();

    PgDb::new(pool)
}

#[tokio::test]
async fn test_one_to_many() {
    let db = db().await;
    let repo = users_repo();
    let bob = User::new(
        "Bob",
        vec!["bob@test.com".to_string(), "bob123@test.com".to_string()],
    );
    repo.add(&db, &bob).await.unwrap();
    let alice = User::new("Alice", vec!["alice@test.com".to_string()]);
    repo.add(&db, &alice).await.unwrap();
    let b = repo.get(&db, &F::eq("id", bob.id)).await.unwrap().unwrap();
    assert_eq!(b, bob);
    let a = repo
        .get(&db, &F::eq("id", alice.id))
        .await
        .unwrap()
        .unwrap();
    assert_eq!(a, alice);
}