use sea_query::{Asterisk, Condition, DynIden, Expr, ExprTrait, Order, Query};
use crate::errors::OrionError;
use crate::storage::models::EntityStatus;
use crate::storage::{DbPool, DbRow, DbTransaction, build_sqlx};
use super::helpers::{
Page, PaginatedResult, Projection, fetch_required, fetch_required_tx, paginate,
};
pub(crate) struct VersionedSpec {
pub table: DynIden,
pub id_col: DynIden,
pub version_col: DynIden,
pub status_col: DynIden,
pub priority_col: DynIden,
pub updated_at_col: DynIden,
pub label: &'static str,
pub noun: &'static str,
}
fn version_select(spec: &VersionedSpec, id: &str, version: i64) -> sea_query::SelectStatement {
Query::select()
.column(Asterisk)
.from(spec.table.clone())
.and_where(Expr::col(spec.id_col.clone()).eq(id))
.and_where(Expr::col(spec.version_col.clone()).eq(version))
.to_owned()
}
fn version_not_found(spec: &VersionedSpec, id: &str, version: i64) -> OrionError {
OrionError::NotFound(format!("{} '{id}' version {version} not found", spec.label))
}
pub(crate) async fn get_version_tx<T: DbRow>(
tx: &mut DbTransaction,
spec: &VersionedSpec,
id: &str,
version: i64,
) -> Result<T, OrionError> {
let (sql, values) = build_sqlx(&mut version_select(spec, id, version));
fetch_required_tx(tx, &sql, values, || version_not_found(spec, id, version)).await
}
pub(crate) async fn write_returning_version<T: DbRow>(
pool: &DbPool,
spec: &VersionedSpec,
write: super::helpers::WriteStatement<'_>,
id: &str,
version: i64,
map_write_err: impl FnOnce(sqlx::Error) -> OrionError,
) -> Result<T, OrionError> {
let mut read_back = version_select(spec, id, version);
super::helpers::write_returning_row(pool, write, &mut read_back, map_write_err, || {
version_not_found(spec, id, version)
})
.await
}
pub(crate) async fn get_latest<T: DbRow>(
pool: &DbPool,
spec: &VersionedSpec,
id: &str,
) -> Result<T, OrionError> {
let (sql, values) = build_sqlx(
Query::select()
.column(Asterisk)
.from(spec.table.clone())
.and_where(Expr::col(spec.id_col.clone()).eq(id))
.order_by(spec.version_col.clone(), Order::Desc)
.limit(1),
);
fetch_required(pool, &sql, values, || {
OrionError::NotFound(format!("{} '{id}' not found", spec.label))
})
.await
}
pub(crate) async fn delete_all_versions(
pool: &DbPool,
spec: &VersionedSpec,
id: &str,
) -> Result<(), OrionError> {
let (sql, values) = build_sqlx(
Query::delete()
.from_table(spec.table.clone())
.and_where(Expr::col(spec.id_col.clone()).eq(id)),
);
if pool.execute_query(&sql, values).await? == 0 {
return Err(OrionError::NotFound(format!(
"{} '{id}' not found",
spec.label
)));
}
Ok(())
}
pub(crate) async fn list_active<T: DbRow>(
pool: &DbPool,
spec: &VersionedSpec,
) -> Result<Vec<T>, OrionError> {
let (sql, values) = build_sqlx(
Query::select()
.column(Asterisk)
.from(spec.table.clone())
.and_where(Expr::col(spec.status_col.clone()).eq(EntityStatus::Active.as_str()))
.order_by(spec.priority_col.clone(), Order::Desc),
);
Ok(pool.fetch_all_as::<T>(&sql, values).await?)
}
pub(crate) async fn list_versions<T: DbRow>(
pool: &DbPool,
spec: &VersionedSpec,
id: &str,
filter: &super::helpers::VersionFilter,
) -> Result<PaginatedResult<T>, OrionError> {
let (limit, offset) = super::helpers::clamp_pagination(filter.limit, filter.offset);
paginate(
pool,
Page {
from: spec.table.clone(),
projection: Projection::All,
cond: Condition::all().add(Expr::col(spec.id_col.clone()).eq(id)),
sort: spec.version_col.clone(),
order: Order::Desc,
limit,
offset,
},
)
.await
}
fn draft_query(spec: &VersionedSpec, id: &str) -> (String, sea_query_sqlx::SqlxValues) {
build_sqlx(
Query::select()
.column(Asterisk)
.from(spec.table.clone())
.and_where(Expr::col(spec.id_col.clone()).eq(id))
.and_where(Expr::col(spec.status_col.clone()).eq(EntityStatus::Draft.as_str())),
)
}
fn no_draft_err(spec: &VersionedSpec, id: &str) -> OrionError {
OrionError::NotFound(format!("No draft version found for {} '{id}'", spec.noun))
}
pub(crate) async fn require_draft<T: DbRow>(
pool: &DbPool,
spec: &VersionedSpec,
id: &str,
) -> Result<T, OrionError> {
let (sql, values) = draft_query(spec, id);
fetch_required(pool, &sql, values, || no_draft_err(spec, id)).await
}
pub(crate) async fn require_draft_tx<T: DbRow>(
tx: &mut DbTransaction,
spec: &VersionedSpec,
id: &str,
) -> Result<T, OrionError> {
let (sql, values) = draft_query(spec, id);
fetch_required_tx(tx, &sql, values, || no_draft_err(spec, id)).await
}
pub(crate) async fn ensure_no_draft<T: DbRow>(
pool: &DbPool,
spec: &VersionedSpec,
id: &str,
) -> Result<(), OrionError> {
let (sql, values) = draft_query(spec, id);
super::helpers::ensure_absent::<T>(pool, &sql, values, || {
OrionError::Conflict(format!("{} '{id}' already has a draft version", spec.label))
})
.await
}
pub(crate) fn archive_actives_query(
spec: &VersionedSpec,
id: &str,
exclude_version: Option<i64>,
) -> sea_query::UpdateStatement {
let mut q = Query::update();
q.table(spec.table.clone())
.value(spec.status_col.clone(), EntityStatus::Archived.as_str())
.and_where(Expr::col(spec.id_col.clone()).eq(id))
.and_where(Expr::col(spec.status_col.clone()).eq(EntityStatus::Active.as_str()));
if let Some(v) = exclude_version {
q.and_where(Expr::col(spec.version_col.clone()).ne(v));
}
q
}
pub(crate) async fn archive_latest_active<T: DbRow + HasVersion>(
pool: &DbPool,
spec: &VersionedSpec,
id: &str,
) -> Result<T, OrionError> {
let no_active =
|| OrionError::NotFound(format!("No active version found for {} '{id}'", spec.noun));
let backend = crate::storage::get_backend();
if backend != crate::storage::DbBackend::Mysql {
let mut update = archive_actives_query(spec, id, None);
if backend == crate::storage::DbBackend::Sqlite {
update.value(
spec.updated_at_col.clone(),
Expr::cust(super::helpers::sql_now(backend)),
);
}
update.returning_all();
let (sql, values) = build_sqlx(&mut update);
let archived: Vec<T> = pool.fetch_all_as(&sql, values).await?;
return archived
.into_iter()
.max_by_key(|row| row.version())
.ok_or_else(no_active);
}
let mut tx = pool.begin_tx().await.map_err(OrionError::Storage)?;
let (sql, values) = build_sqlx(
Query::select()
.column(Asterisk)
.from(spec.table.clone())
.and_where(Expr::col(spec.id_col.clone()).eq(id))
.and_where(Expr::col(spec.status_col.clone()).eq(EntityStatus::Active.as_str()))
.order_by(spec.version_col.clone(), Order::Desc)
.limit(1),
);
let active: T = fetch_required_tx(&mut tx, &sql, values, no_active).await?;
let (sql, values) = build_sqlx(&mut archive_actives_query(spec, id, None));
tx.execute_query(&sql, values).await?;
let archived = get_version_tx(&mut tx, spec, id, active.version()).await?;
tx.commit().await.map_err(OrionError::Storage)?;
Ok(archived)
}
pub(crate) trait HasVersion {
fn version(&self) -> i64;
}