use leptoskit_db::Id;
use leptoskit_db::Json;
use leptoskit_db::Timestamp;
use leptoskit_db::policy;
use leptoskit_db::table;
pub const MAX_REQUEST_ID_LEN: usize = 255;
#[table(
name = "receipts",
pack = "rpc",
unique(owner, request_id),
index(created_at)
)]
#[policy(select, insert, |receipt, cx| receipt.owner.eq(cx.setting::<ReceiptOwner>()))]
pub struct Receipt {
pub id: Id<Self>,
#[db(max_len = 255)]
pub owner: String,
#[db(max_len = 255)]
pub request_id: String,
#[db(max_len = 255)]
pub operation: String,
#[db(max_len = 64)]
pub arguments: String,
pub result: Json<serde_json::Value>,
#[db(created_at)]
pub created_at: Timestamp,
}
pub const ANONYMOUS: &str = "anonymous";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReceiptOwner(pub String);
impl leptoskit_db::Setting for ReceiptOwner {
type Value = String;
const NAME: &'static str = "receipt_owner";
fn value(&self) -> String {
self.0.clone()
}
}
leptoskit_db::register_setting!(ReceiptOwner);
#[cfg(any(feature = "postgres", feature = "sqlite"))]
pub(crate) use running::ReceiptKey;
#[cfg(any(feature = "postgres", feature = "sqlite"))]
pub use running::prune;
#[cfg(any(feature = "postgres", feature = "sqlite"))]
mod running {
use std::fmt::Write;
use leptoskit_db::Db;
use leptoskit_db::DbError;
use leptoskit_db::Json;
use leptoskit_db::Read;
use leptoskit_db::Tx;
use sha2::Digest;
use sha2::Sha256;
use super::MAX_REQUEST_ID_LEN;
use super::NewReceipt;
use super::Receipt;
use crate::Error;
pub(crate) struct ReceiptKey {
pub owner: String,
pub request_id: String,
pub operation: &'static str,
pub arguments: String,
}
impl ReceiptKey {
pub(crate) fn new(
owner: Option<&str>,
request_id: &str,
operation: &'static str,
arguments: &impl serde::Serialize,
) -> Result<Self, Error> {
if request_id.is_empty() || request_id.len() > MAX_REQUEST_ID_LEN {
return Err(Error::invalid(format!(
"an idempotency key is 1 to {MAX_REQUEST_ID_LEN} characters long"
)));
}
let encoded =
serde_json::to_vec(arguments).map_err(|error| Error::invalid(error.to_string()))?;
let mut hash = Sha256::new();
hash.update(operation.as_bytes());
hash.update([0]);
hash.update(&encoded);
let arguments =
hash.finalize()
.iter()
.fold(String::with_capacity(64), |mut hex, byte| {
let _ = write!(hex, "{byte:02x}");
hex
});
Ok(Self {
owner: owner.unwrap_or(super::ANONYMOUS).to_owned(),
request_id: request_id.to_owned(),
operation,
arguments,
})
}
pub(crate) async fn find(
&self,
db: &impl Read,
) -> Result<Option<serde_json::Value>, Error> {
let receipt = Receipt::query()
.filter(|receipt| {
receipt.owner.eq(self.owner.clone())
& receipt.request_id.eq(self.request_id.clone())
})
.first(db)
.await?;
let Some(receipt) = receipt else {
return Ok(None);
};
if receipt.operation != self.operation || receipt.arguments != self.arguments {
return Err(Error::conflict(
"this idempotency key was already used for a different call; use a new key \
for each call",
));
}
Ok(Some(receipt.result.0))
}
pub(crate) async fn write(
&self,
tx: &Tx,
result: serde_json::Value,
) -> Result<(), DbError> {
NewReceipt {
owner: self.owner.clone(),
request_id: self.request_id.clone(),
operation: self.operation.to_owned(),
arguments: self.arguments.clone(),
result: Json(result),
}
.insert(tx)
.await
.map(|_| ())
}
}
pub async fn prune(db: &Db, age: std::time::Duration) -> Result<u64, DbError> {
let age = chrono::Duration::from_std(age).unwrap_or(chrono::Duration::MAX);
let cutoff = leptoskit_db::now()
.checked_sub_signed(age)
.unwrap_or(chrono::DateTime::<chrono::Utc>::MIN_UTC);
Receipt::query()
.filter(|receipt| receipt.created_at.lt(cutoff))
.delete(db)
.await
}
}