use cratefield_core::{Caller, Database, Problem, ProblemDef, Scope, Tenancy, require_admin};
use cratefield_tables::{Owned, TableDef, UpdateError};
use http::{HeaderMap, StatusCode};
use serde_json::{Map, Value};
use crate::access::{Reach, may_write, settle_subject};
use crate::read::{NO_SUCH_ROW, Tables, who};
pub const NOT_A_ROW: ProblemDef = ProblemDef {
slug: "not-a-row",
status: StatusCode::UNPROCESSABLE_ENTITY,
title: "Not a row of this table",
description: "The body does not match the table's declared columns.",
};
pub const ALREADY_EXISTS: ProblemDef = ProblemDef {
slug: "already-exists",
status: StatusCode::CONFLICT,
title: "That key is taken",
description: "A row of this table already has that primary key.",
};
async fn permit(
tables: &Tables,
tenancy: Tenancy,
headers: &HeaderMap,
name: &str,
) -> Result<(crate::access::TableApi, Reach), Problem> {
let api = tables.declared(name).cloned().ok_or_else(|| {
Problem::new(&crate::read::NO_SUCH_TABLE)
})?;
let caller: Caller = who(tables, headers, &api).await?;
let reach = may_write(
&api,
tenancy,
&caller,
require_admin(tables.ctx.config.as_ref(), headers),
)?;
Ok((api, reach))
}
fn owned<'a>(reach: &'a Reach, subject: &'a Value) -> Option<Owned<'a>> {
match reach {
Reach::Everything => None,
Reach::OwnedBy { column, .. } => Some(Owned { column, subject }),
}
}
fn subject_value(reach: &Reach) -> Value {
match reach {
Reach::Everything => Value::Null,
Reach::OwnedBy { subject, .. } => Value::String(subject.clone()),
}
}
pub async fn create(
tables: &Tables,
conn: &dyn Database,
tenancy: Tenancy,
headers: &HeaderMap,
scope: &Scope,
name: &str,
body: Value,
) -> Result<Value, Problem> {
let (api, reach) = permit(tables, tenancy, headers, name).await?;
let mut row = body;
settle_subject(&reach, &mut row)?;
let statement = cratefield_tables::insert(&api.table, &row)
.map_err(|errors| Problem::new(&NOT_A_ROW).with_detail(errors.detail()))?;
match conn.execute(&statement).await {
Ok(_) => Ok(row),
Err(err) if looks_like_a_conflict(&err) => {
Err(taken(conn, &api, &reach, &row, scope).await)
}
Err(err) => {
tracing::error!(error = %err, "a declared table's insert failed");
Err(Problem::internal().instance(&scope.request_id))
}
}
}
async fn taken(
conn: &dyn Database,
api: &crate::access::TableApi,
reach: &Reach,
row: &Value,
scope: &Scope,
) -> Problem {
let subject = subject_value(reach);
let Some(owned_rows) = owned(reach, &subject) else {
return Problem::new(&ALREADY_EXISTS);
};
let Some(key) = key_of(&api.table, row) else {
return Problem::new(&NOT_A_ROW);
};
let statement = match cratefield_tables::select_one(&api.table, &key, Some(owned_rows)) {
Ok(statement) => statement,
Err(err) => {
tracing::error!(
error = %err,
"a declared table's conflict re-select could not be built"
);
return Problem::new(&crate::access::MISDECLARED).instance(&scope.request_id);
}
};
match conn.query(&statement).await {
Ok(rows) if !rows.is_empty() => Problem::new(&ALREADY_EXISTS),
Ok(_) => Problem::new(&NOT_A_ROW),
Err(err) => {
tracing::error!(error = %err, "a declared table's conflict re-select failed");
Problem::internal().instance(&scope.request_id)
}
}
}
fn key_of(table: &TableDef, row: &Value) -> Option<Value> {
let mut key = Map::new();
for column in &table.primary_key {
let value = row.get(column)?;
if value.is_null() {
return None;
}
key.insert(column.clone(), value.clone());
}
Some(Value::Object(key))
}
fn looks_like_a_conflict(err: &cratefield_core::DbError) -> bool {
let text = format!("{err:?}").to_ascii_lowercase();
text.contains("unique constraint failed") || text.contains("violates unique constraint")
}
#[allow(clippy::too_many_arguments)]
pub async fn replace(
tables: &Tables,
conn: &dyn Database,
tenancy: Tenancy,
headers: &HeaderMap,
scope: &Scope,
name: &str,
key: &Value,
body: Value,
) -> Result<Value, Problem> {
let (api, reach) = permit(tables, tenancy, headers, name).await?;
let mut row = body;
settle_subject(&reach, &mut row)?;
let subject = subject_value(&reach);
let statement = cratefield_tables::update(&api.table, key, &row, owned(&reach, &subject))
.map_err(|err| match err {
UpdateError::Row(errors) => Problem::new(&NOT_A_ROW).with_detail(errors.detail()),
UpdateError::Key(err) => {
tracing::error!(error = %err, "a declared table's update could not be built");
Problem::new(&crate::access::MISDECLARED).instance(&scope.request_id)
}
other => {
tracing::error!(error = %other, "a refusal this build does not understand");
Problem::internal().instance(&scope.request_id)
}
})?;
let changed = conn.execute(&statement).await.map_err(|err| {
tracing::error!(error = %err, "a declared table's update failed");
Problem::internal().instance(&scope.request_id)
})?;
if changed == 0 {
return Err(Problem::new(&NO_SUCH_ROW));
}
Ok(row)
}
pub async fn remove(
tables: &Tables,
conn: &dyn Database,
tenancy: Tenancy,
headers: &HeaderMap,
scope: &Scope,
name: &str,
key: &Value,
) -> Result<(), Problem> {
let (api, reach) = permit(tables, tenancy, headers, name).await?;
let subject = subject_value(&reach);
let statement =
cratefield_tables::delete(&api.table, key, owned(&reach, &subject)).map_err(|err| {
tracing::error!(error = %err, "a declared table's delete could not be built");
Problem::new(&crate::access::MISDECLARED).instance(&scope.request_id)
})?;
let removed = conn.execute(&statement).await.map_err(|err| {
tracing::error!(error = %err, "a declared table's delete failed");
Problem::internal().instance(&scope.request_id)
})?;
if removed == 0 {
return Err(Problem::new(&NO_SUCH_ROW));
}
Ok(())
}