use std::sync::Arc;
use aion::Engine;
use aion_core::{Event, PayloadElision, WorkflowId};
use aion_proto::{
ProtoDescribeWorkflowRequest, ProtoReadHistoryRequest, ProtoWorkflowId, WireError,
};
use axum::{Json, extract::State};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use uuid::Uuid;
use super::auth::HttpCaller;
use super::error::HttpWireError;
use crate::{NamespaceOperation, ServerError, ServerState, WorkflowTarget};
const DEFAULT_PAYLOAD_LIMIT_BYTES: u32 = 2_048;
#[derive(Debug, Deserialize)]
pub(crate) struct HistoryFetchRequest {
namespace: String,
workflow_id: String,
#[serde(default)]
from_seq: Option<u64>,
#[serde(default)]
limit: Option<u32>,
#[serde(default)]
payload_limit_bytes: Option<u32>,
}
#[derive(Debug, Serialize)]
pub(crate) struct HistoryFetchResponse {
events: Vec<Value>,
next_from_seq: Option<u64>,
head_seq: u64,
}
#[derive(Debug, Deserialize)]
pub(crate) struct EventFetchRequest {
namespace: String,
workflow_id: String,
seq: u64,
}
#[derive(Debug, Serialize)]
pub(crate) struct EventFetchResponse {
event: Event,
}
pub(crate) async fn fetch_history(
State(state): State<ServerState>,
HttpCaller(caller): HttpCaller,
Json(request): Json<HistoryFetchRequest>,
) -> Result<Json<HistoryFetchResponse>, HttpWireError> {
parse_workflow_id(&request.workflow_id)?;
let window = crate::api::handlers::history_window(
state.namespace_guard(),
&caller,
ProtoReadHistoryRequest {
namespace: request.namespace,
workflow_id: Some(ProtoWorkflowId {
uuid: request.workflow_id,
}),
from_seq: request.from_seq,
limit: request.limit,
},
)
.await
.map_err(HttpWireError)?;
let payload_limit = request
.payload_limit_bytes
.map_or(DEFAULT_PAYLOAD_LIMIT_BYTES, |limit| limit);
let events = window
.events
.into_iter()
.map(|event| project_event(event, payload_limit))
.collect::<Result<Vec<_>, _>>()?;
Ok(Json(HistoryFetchResponse {
events,
next_from_seq: window.next_from_seq,
head_seq: window.head_seq,
}))
}
pub(crate) async fn fetch_event(
State(state): State<ServerState>,
HttpCaller(caller): HttpCaller,
Json(request): Json<EventFetchRequest>,
) -> Result<Json<EventFetchResponse>, HttpWireError> {
let workflow_id = parse_workflow_id(&request.workflow_id)?;
let engine = scoped_engine(
&state,
&caller,
&request.namespace,
&request.workflow_id,
&workflow_id,
)
.await?;
let event = engine
.store()
.read_history_from(&workflow_id, request.seq)
.await
.map_err(store_error)?
.into_iter()
.find(|event| event.seq() == request.seq)
.ok_or_else(|| {
HttpWireError(WireError::not_found(format!(
"workflow event {} at seq {} was not found",
request.workflow_id, request.seq
)))
})?;
Ok(Json(EventFetchResponse { event }))
}
pub(super) async fn scoped_engine(
state: &ServerState,
caller: &crate::CallerIdentity,
namespace: &str,
workflow_id_wire: &str,
workflow_id: &WorkflowId,
) -> Result<Arc<Engine>, HttpWireError> {
let describe = ProtoDescribeWorkflowRequest {
namespace: namespace.to_owned(),
workflow_id: Some(ProtoWorkflowId {
uuid: workflow_id_wire.to_owned(),
}),
run_id: None,
include_history: false,
};
let target = WorkflowTarget::workflow(workflow_id);
let scoped = state
.namespace_guard()
.scope(caller, &NamespaceOperation::describe(&describe, target))
.await
.map_err(|error| HttpWireError(error.to_wire_error()))?;
scoped
.engine()
.cloned()
.map_err(|error| HttpWireError(error.to_wire_error()))
}
fn parse_workflow_id(value: &str) -> Result<WorkflowId, HttpWireError> {
Uuid::parse_str(value)
.map(WorkflowId::new)
.map_err(|_error| HttpWireError(WireError::invalid_input("workflow_id must be a UUID")))
}
fn store_error(error: aion_store::StoreError) -> HttpWireError {
HttpWireError(ServerError::from(error).to_wire_error())
}
fn project_event(event: Event, payload_limit: u32) -> Result<Value, HttpWireError> {
let mut value = serde_json::to_value(event).map_err(|error| {
HttpWireError(WireError::backend(format!(
"workflow event JSON encoding failed: {error}"
)))
})?;
if payload_limit != 0 {
elide_payload_bytes(&mut value, u64::from(payload_limit))?;
}
Ok(value)
}
fn elide_payload_bytes(value: &mut Value, limit: u64) -> Result<(), HttpWireError> {
match value {
Value::Array(values) => {
for value in values {
elide_payload_bytes(value, limit)?;
}
}
Value::Object(object) => {
if is_payload_object(object) {
if let Some(bytes) = object.get_mut("bytes")
&& let Some(size_bytes) = payload_byte_len(bytes).filter(|size| *size > limit)
{
*bytes = elision_value(size_bytes)?;
}
} else {
for value in object.values_mut() {
elide_payload_bytes(value, limit)?;
}
}
}
_ => {}
}
Ok(())
}
fn elision_value(size_bytes: u64) -> Result<Value, HttpWireError> {
serde_json::to_value(PayloadElision::new(size_bytes)).map_err(|error| {
HttpWireError(WireError::backend(format!(
"payload elision encoding failed: {error}"
)))
})
}
fn is_payload_object(object: &Map<String, Value>) -> bool {
object.get("content_type").is_some_and(Value::is_string) && object.contains_key("bytes")
}
fn payload_byte_len(value: &Value) -> Option<u64> {
value
.as_array()
.and_then(|bytes| u64::try_from(bytes.len()).ok())
}