use crate::audit;
use crate::authz::Action;
use crate::error::CoreError;
use crate::request::ReqCtx;
use crate::resource::Resource;
use crate::response::ApiResponse;
use crate::storage::{storage, CreateOutcome};
use serde_json::{json, Value};
pub async fn create<R: Resource + ?Sized>(res: &R, ctx: &ReqCtx, body: Value) -> ApiResponse {
if !res.authorize(ctx, Action::Create) {
return CoreError::Unauthorized.into();
}
let data = match res.filter_writable(body) {
Ok(d) => d,
Err(resp) => return resp,
};
let snapshot = audit::is_enabled().then(|| data.clone());
match storage().create(res.table_name(), data).await {
Ok(CreateOutcome { last_insert_id }) => {
if let Some(written) = snapshot {
let entry = audit::AuditEntry::new(
ctx,
res.base_path(),
last_insert_id.clone().unwrap_or_default(),
audit::Event::Create,
audit::diff(None, &written),
);
if let Some(reject) = audit::emit(entry).await {
return reject;
}
}
reindex_after_write(res, last_insert_id.as_deref()).await;
ApiResponse::created(json!({
"success": true,
"message": format!("{} created successfully", res.resource_name()),
"last_insert_id": last_insert_id,
}))
}
Err(e) => CoreError::from(e).into(),
}
}
async fn reindex_after_write<R: Resource + ?Sized>(res: &R, id: Option<&str>) {
let fields = res.search_fields();
if fields.is_empty() || !crate::search::is_enabled() {
return;
}
let Some(id) = id else { return };
if let Ok(Some(row)) = storage().get(res.table_name(), res.primary_key(), id).await {
let doc = crate::search::document_for(&row, &fields);
crate::search::index_record(res.base_path(), id, doc).await;
}
}
pub async fn update<R: Resource + ?Sized>(
res: &R,
ctx: &ReqCtx,
id: &str,
body: Value,
) -> ApiResponse {
if !res.authorize(ctx, Action::Update) {
return CoreError::Unauthorized.into();
}
let data = match res.filter_writable(body) {
Ok(d) => d,
Err(resp) => return resp,
};
let (before, snapshot) = if audit::is_enabled() {
let before = storage()
.get(res.table_name(), res.primary_key(), id)
.await
.unwrap_or(None);
(before, Some(data.clone()))
} else {
(None, None)
};
match storage()
.update(res.table_name(), res.primary_key(), id, data)
.await
{
Ok(n) if n > 0 => {
if let Some(written) = snapshot {
let changes = audit::diff(before.as_ref(), &written);
if !changes.is_empty() {
let entry = audit::AuditEntry::new(
ctx,
res.base_path(),
id,
audit::Event::Update,
changes,
);
if let Some(reject) = audit::emit(entry).await {
return reject;
}
}
}
reindex_after_write(res, Some(id)).await;
ApiResponse::ok(json!({
"success": true,
"message": format!("{} updated successfully", res.resource_name()),
"modified_count": n,
}))
}
Ok(_) => CoreError::NotFound.into(),
Err(e) => CoreError::from(e).into(),
}
}
pub async fn delete<R: Resource + ?Sized>(res: &R, ctx: &ReqCtx, id: &str) -> ApiResponse {
if !res.authorize(ctx, Action::Delete) {
return CoreError::Unauthorized.into();
}
let soft = res.soft_delete();
let before = if audit::is_enabled() {
storage()
.get(res.table_name(), res.primary_key(), id)
.await
.unwrap_or(None)
} else {
None
};
match storage()
.delete(res.table_name(), res.primary_key(), id, soft)
.await
{
Ok(n) if n > 0 => {
if audit::is_enabled() {
let changes = before.as_ref().map(audit::diff_removed).unwrap_or_default();
let entry =
audit::AuditEntry::new(ctx, res.base_path(), id, audit::Event::Delete, changes);
if let Some(reject) = audit::emit(entry).await {
return reject;
}
}
if !soft && crate::attach::is_enabled() {
crate::attach::purge(res.base_path(), id).await;
}
if crate::search::is_enabled() && !res.search_fields().is_empty() {
crate::search::remove_record(res.base_path(), id).await;
}
ApiResponse::ok(json!({
"success": true,
"message": format!("{} deleted successfully", res.resource_name()),
"soft_delete": soft,
"affected": n,
}))
}
Ok(_) => CoreError::NotFound.into(),
Err(e) => CoreError::from(e).into(),
}
}