use cratefield_core::{Database, DbError, Row, Statement};
use sea_query::{
Alias, Condition, Expr, JoinType, OnConflict, Order, Query, SelectStatement, SimpleExpr,
};
pub(crate) const ORGS: &str = "orgs";
pub(crate) const MEMBERS: &str = "org_members";
pub(crate) const INVITATIONS: &str = "org_invitations";
pub(crate) const OWNER: &str = "owner";
fn iden(name: &str) -> Alias {
Alias::new(name)
}
fn another_owner_remains() -> SimpleExpr {
Expr::cust(format!(
"(SELECT COUNT(*) FROM {MEMBERS} m WHERE m.org_id = {MEMBERS}.org_id \
AND m.role = '{OWNER}') > 1"
))
}
fn actor_member_select(actor: &str) -> SelectStatement {
let actor_member = Alias::new("actor_member");
let mut select = Query::select();
select
.expr(Expr::val(1))
.from_as(iden(MEMBERS), actor_member.clone())
.and_where(
Expr::col((actor_member.clone(), iden("org_id")))
.eq(Expr::col((MEMBERS, iden("org_id")))),
)
.and_where(Expr::col((actor_member, iden("user_sub"))).eq(actor));
select
}
fn actor_may_manage(actor: &str, managers: &[String]) -> SimpleExpr {
let mut roles: Vec<&str> = managers.iter().map(String::as_str).collect();
if !managers.iter().any(|role| role == OWNER) {
roles.push(OWNER);
}
let mut select = actor_member_select(actor);
select.and_where(Expr::col((Alias::new("actor_member"), iden("role"))).is_in(roles));
Expr::exists(select)
}
fn actor_is_owner(actor: &str) -> SimpleExpr {
let mut select = actor_member_select(actor);
select.and_where(Expr::col((Alias::new("actor_member"), iden("role"))).eq(OWNER));
Expr::exists(select)
}
pub(crate) fn lock_org_statement(org_id: &str) -> Statement {
Statement::with_values("UPDATE orgs SET id = id WHERE id = ?", vec![org_id.into()])
}
pub(crate) struct OrgRow {
pub(crate) id: String,
pub(crate) name: String,
pub(crate) created_by: String,
pub(crate) created_at: String,
}
const ORG_COLUMNS: [&str; 4] = ["id", "name", "created_by", "created_at"];
fn org_from(row: &Row) -> OrgRow {
OrgRow {
id: row.get("id").unwrap_or_default(),
name: row.get("name").unwrap_or_default(),
created_by: row.get("created_by").unwrap_or_default(),
created_at: row.get("created_at").unwrap_or_default(),
}
}
pub(crate) fn insert_org_statement(id: &str, name: &str, created_by: &str, now: &str) -> Statement {
let mut insert = Query::insert();
insert
.into_table(iden(ORGS))
.columns(["id", "name", "created_by", "created_at"])
.values_panic([id.into(), name.into(), created_by.into(), now.into()]);
Statement::render(&insert)
}
pub(crate) async fn find_org(db: &dyn Database, id: &str) -> Result<Option<OrgRow>, DbError> {
let mut select = Query::select();
select
.columns(ORG_COLUMNS)
.from(iden(ORGS))
.and_where(Expr::col(iden("id")).eq(id));
Ok(db
.query(&Statement::render(&select))
.await?
.first()
.map(org_from))
}
pub(crate) async fn count_owners(db: &dyn Database, org_id: &str) -> Result<i64, DbError> {
let mut select = Query::select();
select
.expr_as(Expr::col(iden("user_sub")).count(), iden("n"))
.from(iden(MEMBERS))
.and_where(Expr::col(iden("org_id")).eq(org_id))
.and_where(Expr::col(iden("role")).eq(OWNER));
Ok(db
.query(&Statement::render(&select))
.await?
.first()
.and_then(|row| row.get("n"))
.unwrap_or(0))
}
fn org_page_cursor(select: &mut sea_query::SelectStatement, after: Option<(&str, &str)>) {
if let Some((at, id)) = after {
select.cond_where(
Condition::any()
.add(Expr::col((iden(ORGS), iden("created_at"))).gt(at))
.add(
Condition::all()
.add(Expr::col((iden(ORGS), iden("created_at"))).eq(at))
.add(Expr::col((iden(ORGS), iden("id"))).gt(id)),
),
);
}
}
pub(crate) async fn list_all_orgs(
db: &dyn Database,
after: Option<(&str, &str)>,
fetch: u64,
) -> Result<Vec<OrgRow>, DbError> {
let mut select = Query::select();
select
.columns(ORG_COLUMNS)
.from(iden(ORGS))
.order_by(iden("created_at"), Order::Asc)
.order_by(iden("id"), Order::Asc)
.limit(fetch);
org_page_cursor(&mut select, after);
Ok(db
.query(&Statement::render(&select))
.await?
.rows
.iter()
.map(org_from)
.collect())
}
pub(crate) async fn list_orgs_for(
db: &dyn Database,
sub: &str,
after: Option<(&str, &str)>,
fetch: u64,
) -> Result<Vec<(OrgRow, String)>, DbError> {
let mut select = Query::select();
select
.expr_as(Expr::col((iden(ORGS), iden("id"))), iden("id"))
.expr_as(Expr::col((iden(ORGS), iden("name"))), iden("name"))
.expr_as(
Expr::col((iden(ORGS), iden("created_by"))),
iden("created_by"),
)
.expr_as(
Expr::col((iden(ORGS), iden("created_at"))),
iden("created_at"),
)
.expr_as(Expr::col((iden(MEMBERS), iden("role"))), iden("role"))
.from(iden(ORGS))
.join(
JoinType::InnerJoin,
iden(MEMBERS),
Expr::col((iden(MEMBERS), iden("org_id"))).equals((iden(ORGS), iden("id"))),
)
.and_where(Expr::col((iden(MEMBERS), iden("user_sub"))).eq(sub))
.order_by((iden(ORGS), iden("created_at")), Order::Asc)
.order_by((iden(ORGS), iden("id")), Order::Asc)
.limit(fetch);
if let Some((at, id)) = after {
select.cond_where(
Condition::any()
.add(Expr::col((iden(ORGS), iden("created_at"))).gt(at))
.add(
Condition::all()
.add(Expr::col((iden(ORGS), iden("created_at"))).eq(at))
.add(Expr::col((iden(ORGS), iden("id"))).gt(id)),
),
);
}
Ok(db
.query(&Statement::render(&select))
.await?
.rows
.iter()
.map(|row| (org_from(row), row.get("role").unwrap_or_default()))
.collect())
}
pub(crate) struct MemberRow {
pub(crate) sub: String,
pub(crate) role: String,
pub(crate) invited_by: Option<String>,
pub(crate) created_at: String,
}
fn member_from(row: &Row) -> MemberRow {
MemberRow {
sub: row.get("user_sub").unwrap_or_default(),
role: row.get("role").unwrap_or_default(),
invited_by: row.get("invited_by"),
created_at: row.get("created_at").unwrap_or_default(),
}
}
pub(crate) fn insert_member_statement(
org_id: &str,
sub: &str,
role: &str,
invited_by: Option<&str>,
now: &str,
) -> Statement {
let mut insert = Query::insert();
insert
.into_table(iden(MEMBERS))
.columns(["org_id", "user_sub", "role", "invited_by", "created_at"])
.values_panic([
org_id.into(),
sub.into(),
role.into(),
invited_by.into(),
now.into(),
]);
Statement::render(&insert)
}
pub(crate) async fn insert_member_once(
db: &dyn Database,
org_id: &str,
sub: &str,
role: &str,
invited_by: Option<&str>,
now: &str,
) -> Result<u64, DbError> {
db.execute(&insert_member_once_statement(
org_id, sub, role, invited_by, now,
))
.await
}
pub(crate) fn insert_member_once_statement(
org_id: &str,
sub: &str,
role: &str,
invited_by: Option<&str>,
now: &str,
) -> Statement {
let mut insert = Query::insert();
insert
.into_table(iden(MEMBERS))
.columns(["org_id", "user_sub", "role", "invited_by", "created_at"])
.values_panic([
org_id.into(),
sub.into(),
role.into(),
invited_by.into(),
now.into(),
])
.on_conflict(
OnConflict::columns([iden("org_id"), iden("user_sub")])
.do_nothing()
.to_owned(),
);
Statement::render(&insert)
}
pub(crate) async fn find_member(
db: &dyn Database,
org_id: &str,
sub: &str,
) -> Result<Option<MemberRow>, DbError> {
let mut select = Query::select();
select
.columns(["user_sub", "role", "invited_by", "created_at"])
.from(iden(MEMBERS))
.and_where(Expr::col(iden("org_id")).eq(org_id))
.and_where(Expr::col(iden("user_sub")).eq(sub));
Ok(db
.query(&Statement::render(&select))
.await?
.first()
.map(member_from))
}
pub(crate) async fn list_members(
db: &dyn Database,
org_id: &str,
after: Option<(&str, &str)>,
fetch: u64,
) -> Result<Vec<MemberRow>, DbError> {
let mut select = Query::select();
select
.columns(["user_sub", "role", "invited_by", "created_at"])
.from(iden(MEMBERS))
.and_where(Expr::col(iden("org_id")).eq(org_id))
.order_by(iden("created_at"), sea_query::Order::Asc)
.order_by(iden("user_sub"), sea_query::Order::Asc)
.limit(fetch);
if let Some((at, sub)) = after {
select.cond_where(
Condition::any()
.add(Expr::col(iden("created_at")).gt(at))
.add(
Condition::all()
.add(Expr::col(iden("created_at")).eq(at))
.add(Expr::col(iden("user_sub")).gt(sub)),
),
);
}
Ok(db
.query(&Statement::render(&select))
.await?
.rows
.iter()
.map(member_from)
.collect())
}
pub(crate) fn update_role_statements(
org_id: &str,
sub: &str,
role: &str,
actor: &str,
managers: &[String],
) -> [Statement; 2] {
let mut update = Query::update();
update
.table(iden(MEMBERS))
.value(iden("role"), role)
.and_where(Expr::col(iden("org_id")).eq(org_id))
.and_where(Expr::col(iden("user_sub")).eq(sub))
.cond_where(actor_may_manage(actor, managers))
.cond_where(
Condition::any()
.add(Expr::col(iden("role")).ne(OWNER))
.add(actor_is_owner(actor)),
)
.cond_where(
Condition::any()
.add(Expr::col(iden("role")).ne(OWNER))
.add(Expr::col(iden("role")).eq(role))
.add(another_owner_remains()),
);
if role == OWNER {
update.cond_where(actor_is_owner(actor));
}
[lock_org_statement(org_id), Statement::render(&update)]
}
pub(crate) fn remove_member_statements(
org_id: &str,
sub: &str,
actor: &str,
managers: &[String],
) -> [Statement; 2] {
let mut delete = Query::delete();
delete
.from_table(iden(MEMBERS))
.and_where(Expr::col(iden("org_id")).eq(org_id))
.and_where(Expr::col(iden("user_sub")).eq(sub))
.cond_where(
Condition::any()
.add(Expr::col(iden("role")).ne(OWNER))
.add(another_owner_remains()),
);
if actor != sub {
delete.cond_where(
Condition::all().add(actor_may_manage(actor, managers)).add(
Condition::any()
.add(Expr::col(iden("role")).ne(OWNER))
.add(actor_is_owner(actor)),
),
);
}
[lock_org_statement(org_id), Statement::render(&delete)]
}
pub(crate) struct InvitationRow {
pub(crate) org_id: String,
pub(crate) email_hash: String,
pub(crate) role: String,
pub(crate) invited_by: String,
pub(crate) expires_at: String,
pub(crate) accepted_at: Option<String>,
pub(crate) spend_id: Option<String>,
}
const INVITATION_COLUMNS: [&str; 7] = [
"org_id",
"email_hash",
"role",
"invited_by",
"expires_at",
"accepted_at",
"spend_id",
];
fn invitation_from(row: &Row) -> InvitationRow {
InvitationRow {
org_id: row.get("org_id").unwrap_or_default(),
email_hash: row.get("email_hash").unwrap_or_default(),
role: row.get("role").unwrap_or_default(),
invited_by: row.get("invited_by").unwrap_or_default(),
expires_at: row.get("expires_at").unwrap_or_default(),
accepted_at: row.get("accepted_at"),
spend_id: row.get("spend_id"),
}
}
pub(crate) struct NewInvitation<'a> {
pub(crate) id: &'a str,
pub(crate) org_id: &'a str,
pub(crate) token_hash: &'a str,
pub(crate) email_hash: &'a str,
pub(crate) role: &'a str,
pub(crate) invited_by: &'a str,
pub(crate) expires_at: &'a str,
pub(crate) now: &'a str,
}
pub(crate) async fn insert_invitation(
db: &dyn Database,
invitation: &NewInvitation<'_>,
) -> Result<u64, DbError> {
let mut insert = Query::insert();
insert
.into_table(iden(INVITATIONS))
.columns([
"id",
"org_id",
"token_hash",
"email_hash",
"role",
"invited_by",
"expires_at",
"created_at",
])
.values_panic([
invitation.id.into(),
invitation.org_id.into(),
invitation.token_hash.into(),
invitation.email_hash.into(),
invitation.role.into(),
invitation.invited_by.into(),
invitation.expires_at.into(),
invitation.now.into(),
]);
db.execute(&Statement::render(&insert)).await
}
pub(crate) async fn find_invitation(
db: &dyn Database,
token_hash: &str,
) -> Result<Option<InvitationRow>, DbError> {
let mut select = Query::select();
select
.columns(INVITATION_COLUMNS)
.from(iden(INVITATIONS))
.and_where(Expr::col(iden("token_hash")).eq(token_hash));
Ok(db
.query(&Statement::render(&select))
.await?
.first()
.map(invitation_from))
}
pub(crate) fn accept_statements(
org_id: &str,
sub: &str,
role: &str,
invited_by: &str,
now: &str,
token_hash: &str,
spend_id: &str,
) -> [Statement; 2] {
let mut spend = Query::update();
spend
.table(iden(INVITATIONS))
.value(iden("accepted_at"), now)
.value(iden("spend_id"), spend_id)
.and_where(Expr::col(iden("token_hash")).eq(token_hash))
.and_where(Expr::col(iden("accepted_at")).is_null())
.and_where(Expr::col(iden("expires_at")).gt(now));
let member = Statement::with_values(
format!(
"INSERT INTO {MEMBERS} (org_id, user_sub, role, invited_by, created_at) \
SELECT ?, ?, ?, ?, ? WHERE EXISTS \
(SELECT 1 FROM {INVITATIONS} WHERE spend_id = ?) \
ON CONFLICT (org_id, user_sub) DO NOTHING"
),
vec![
org_id.into(),
sub.into(),
role.into(),
invited_by.into(),
now.into(),
spend_id.into(),
],
);
[Statement::render(&spend), member]
}
pub(crate) async fn delete_invitation(db: &dyn Database, id: &str) -> Result<u64, DbError> {
let mut delete = Query::delete();
delete
.from_table(iden(INVITATIONS))
.and_where(Expr::col(iden("id")).eq(id));
db.execute(&Statement::render(&delete)).await
}
pub(crate) async fn purge_invitations(db: &dyn Database, now: &str) -> Result<u64, DbError> {
let mut delete = Query::delete();
delete
.from_table(iden(INVITATIONS))
.and_where(Expr::col(iden("expires_at")).lte(now));
db.execute(&Statement::render(&delete)).await
}