use std::sync::Arc;
use myko::command::{CommandContext, CommandError, CommandHandler};
use myko::prelude::*;
use myko::saga::{SagaContext, SagaHandler};
use myko::wire::MEventType;
use crate::entities::{
Applied, GetAppliedsByIds, GetLogEntrysByQuery, LogEntry, PartialLogEntry, applied_id,
};
#[myko_command]
pub struct ApplyLogEntry {
pub cbor_b64: String,
pub event_oid: String,
}
impl CommandHandler for ApplyLogEntry {
fn execute(self, ctx: CommandContext) -> Result<(), CommandError> {
use base64::Engine as _;
let err = |m: String| CommandError {
tx: ctx.tx().to_string(),
command_id: ctx.command_id.to_string(),
message: m,
};
let bytes = base64::engine::general_purpose::STANDARD
.decode(&self.cbor_b64)
.map_err(|e| err(format!("invalid base64 in LogEntry: {e}")))?;
let event: myko::wire::MEvent = ciborium::from_reader(bytes.as_slice())
.map_err(|e| err(format!("invalid CBOR in LogEntry: {e}")))?;
if event.item_type == LogEntry::ENTITY_NAME_STATIC {
return Err(err("refusing to unwrap a nested LogEntry".into()));
}
let Some(item_id) = event.item.get("id").and_then(|v| v.as_str()) else {
return Err(err("wrapped event has no item id".into()));
};
let guard_id = applied_id(&event.item_type, item_id);
let existing = ctx.exec_query_first(GetAppliedsByIds {
ids: vec![guard_id.clone().into()],
})?;
let incoming = (event.created_at.clone(), self.event_oid.clone());
if let Some(applied) = existing
&& (applied.event_created.as_str(), applied.event_oid.as_str())
> (incoming.0.as_str(), incoming.1.as_str())
{
return Ok(()); }
ctx.emit_event_batch(vec![event])?;
ctx.emit_set(&Applied {
id: guard_id.into(),
event_created: incoming.0,
event_oid: incoming.1,
})?;
Ok(())
}
}
myko::register_command_handler!(ApplyLogEntry);
#[myko_saga]
pub struct UnwrapLogEntries;
impl SagaHandler for UnwrapLogEntries {
type EventItem = LogEntry;
type Command = ApplyLogEntry;
const EVENT_TYPE: MEventType = MEventType::SET;
fn handle(
item: LogEntry,
_event: myko::wire::MEvent,
_ctx: Arc<SagaContext>,
) -> Option<ApplyLogEntry> {
Some(ApplyLogEntry {
cbor_b64: item.cbor_b64,
event_oid: item.id.to_string(),
})
}
}
#[myko_report_output]
pub struct LogEntryBucketsOut {
pub buckets: std::collections::BTreeMap<String, String>,
}
#[myko_report(LogEntryBucketsOut)]
pub struct LogEntryBuckets {
pub project_id: String,
}
impl myko::report::ReportHandler for LogEntryBuckets {
type Output = LogEntryBucketsOut;
#[cfg(not(target_arch = "wasm32"))]
fn compute(
&self,
ctx: myko::report::ReportContext,
) -> impl myko::hyphae::MaterializeDefinite<Arc<Self::Output>> {
ctx.query_map(GetLogEntrysByQuery(PartialLogEntry {
project_id: Some(self.project_id.clone()),
..Default::default()
}))
.items()
.map(|entries| {
Arc::new(LogEntryBucketsOut {
buckets: crate::merkle::bucket_hashes(entries.iter().map(|e| &*e.id.0)),
})
})
}
}
#[myko_report_output]
pub struct LogEntryBucketIdsOut {
pub ids: Vec<String>,
}
#[myko_report(LogEntryBucketIdsOut)]
pub struct LogEntryBucketIds {
pub project_id: String,
pub bucket: String,
}
impl myko::report::ReportHandler for LogEntryBucketIds {
type Output = LogEntryBucketIdsOut;
#[cfg(not(target_arch = "wasm32"))]
fn compute(
&self,
ctx: myko::report::ReportContext,
) -> impl myko::hyphae::MaterializeDefinite<Arc<Self::Output>> {
let bucket = self.bucket.clone();
ctx.query_map(GetLogEntrysByQuery(PartialLogEntry {
project_id: Some(self.project_id.clone()),
..Default::default()
}))
.items()
.map(move |entries| {
let mut ids: Vec<String> = entries
.iter()
.map(|e| e.id.to_string())
.filter(|id| id.starts_with(&bucket))
.collect();
ids.sort_unstable();
Arc::new(LogEntryBucketIdsOut { ids })
})
}
}