use cratestack_core::{AuditOperation, CoolContext, CoolError, ModelEventKind};
use crate::audit::{build_audit_event, enqueue_audit_event, ensure_audit_table};
use crate::descriptor::{enqueue_event_outbox, ensure_event_outbox_table};
use crate::query::support::evaluate_create_policies;
use crate::{ConflictTarget, ModelDescriptor, SqlValue, SqlxRuntime, UpsertModelInput, sqlx};
use super::upsert_do_nothing_sql::upsert_returning_record_do_nothing;
use super::upsert_exec::prepare_upsert_insert;
use super::upsert_outcome::UpsertOutcome;
use super::upsert_sql::{row_passes_update_policy, select_for_update_by_conflict_target};
pub(super) async fn run_upsert_do_nothing_in_tx<'tx, M, PK, I>(
tx: &mut sqlx::Transaction<'tx, sqlx::Postgres>,
runtime: &SqlxRuntime,
descriptor: &'static ModelDescriptor<M, PK>,
input: I,
conflict_target: ConflictTarget,
ctx: &CoolContext,
) -> Result<(UpsertOutcome<M>, bool), CoolError>
where
I: UpsertModelInput<M>,
for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
{
input.validate()?;
let (insert_values, conflict_columns) =
prepare_upsert_insert(descriptor, &input, ctx, conflict_target)?;
if !evaluate_create_policies(
runtime.pool(),
descriptor.create_allow_policies,
descriptor.create_deny_policies,
&insert_values,
ctx,
)
.await?
{
return Err(CoolError::Forbidden(
"create policy denied this upsert".to_owned(),
));
}
let emits_created = descriptor.emits(ModelEventKind::Created);
let audit_enabled = descriptor.audit_enabled;
if emits_created {
ensure_event_outbox_table(&mut **tx).await?;
}
if audit_enabled {
ensure_audit_table(runtime).await?;
}
if let Some(existing) =
select_for_update_by_conflict_target(&mut **tx, descriptor, &conflict_columns).await?
{
authorize_existing_row(runtime, descriptor, &conflict_columns, ctx).await?;
return Ok((UpsertOutcome::Existing(existing), false));
}
match upsert_returning_record_do_nothing(&mut **tx, descriptor, &insert_values, conflict_target)
.await?
{
Some(record) => {
if emits_created {
enqueue_event_outbox(
&mut **tx,
descriptor.schema_name,
ModelEventKind::Created,
&record,
)
.await?;
}
if audit_enabled {
let after = serde_json::to_value(&record).ok();
let event = build_audit_event(descriptor, AuditOperation::Create, None, after, ctx);
enqueue_audit_event(&mut **tx, &event).await?;
}
Ok((UpsertOutcome::Inserted(record), emits_created))
}
None => {
let existing =
select_for_update_by_conflict_target(&mut **tx, descriptor, &conflict_columns)
.await?
.ok_or_else(|| {
CoolError::Conflict(format!(
"upsert do_nothing on `{}` lost a conflict race and the \
conflicting row was deleted before it could be read back; retry the call",
descriptor.table_name,
))
})?;
authorize_existing_row(runtime, descriptor, &conflict_columns, ctx).await?;
Ok((UpsertOutcome::Existing(existing), false))
}
}
}
async fn authorize_existing_row<M, PK>(
runtime: &SqlxRuntime,
descriptor: &'static ModelDescriptor<M, PK>,
conflict_columns: &[(&'static str, SqlValue)],
ctx: &CoolContext,
) -> Result<(), CoolError> {
if !row_passes_update_policy(runtime.pool(), descriptor, conflict_columns, ctx).await? {
return Err(CoolError::Forbidden(
"update policy denied this upsert".to_owned(),
));
}
Ok(())
}