use super::{Database, Error, FilterableQuery, NodeKind};
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateKind {
Input,
Approval,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GateWait {
pub node_id: String,
pub activation_id: String,
pub requested_at_ms: u64,
pub input: serde_json::Value,
pub kind: GateKind,
pub prompt: Option<String>,
pub response_schema: Option<bcode_workflow::ValueSchema>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatePage {
pub revision: u64,
pub gates: Vec<GateWait>,
pub next_node_id: Option<String>,
}
pub async fn page(db: &dyn Database, run: &str, limit: u16) -> Result<GatePage, Error> {
page_after(db, run, None, None, limit).await
}
pub async fn page_after(
db: &dyn Database,
run: &str,
expected_revision: Option<u64>,
after: Option<&str>,
limit: u16,
) -> Result<GatePage, Error> {
let tx = db.begin_transaction().await?;
let result = page_in(&*tx, run, expected_revision, after, limit).await?;
tx.commit().await?;
Ok(result)
}
pub async fn page_in(
tx: &dyn switchy_database::DatabaseTransaction,
run: &str,
expected_revision: Option<u64>,
after: Option<&str>,
limit: u16,
) -> Result<GatePage, Error> {
if !(1..=100).contains(&limit) || (after.is_some() && expected_revision.is_none()) {
return Err(Error::InvalidAdmission);
}
super::super::verified(tx).await?;
let revision = super::super::graph_pages::revision(tx, run).await?;
if expected_revision.is_some_and(|v| v != revision) {
return Err(Error::InvalidAdmission);
}
let roots = tx
.select(super::RUNS)
.columns(&["status"])
.where_eq("run_id", run)
.limit(1)
.execute(tx)
.await?;
let root = roots.first().ok_or(Error::InvalidAdmission)?;
let status = super::super::runs::RunState::decode(&super::text(root, "status")?)?;
let blocks = super::super::safety::admission_blocks(tx, run).await?;
let mut result = GatePage {
revision,
gates: Vec::new(),
next_node_id: None,
};
if status != super::super::runs::RunState::Paused
|| blocks.cancelled
|| blocks.control_failure
|| blocks.recovery
|| blocks.user_paused
{
return Ok(result);
}
let activations = super::ready_in(tx, run, revision, after, limit).await?;
if let Some(last) = activations.last()
&& !super::ready_in(tx, run, revision, Some(&last.node_id), 1)
.await?
.is_empty()
{
result.next_node_id = Some(last.node_id.clone());
}
for activation in activations {
if !super::is_ready_activation(tx, run, &activation.node_id, &activation.activation_id)
.await?
{
continue;
}
let node = super::execution_node(tx, run, &activation.node_id).await?;
let kind = match node.kind {
NodeKind::Input => GateKind::Input,
NodeKind::Approval => GateKind::Approval,
_ => continue,
};
let prompt = match node.configuration.get("prompt") {
None | Some(serde_json::Value::Null) => None,
Some(serde_json::Value::String(v)) => Some(v.clone()),
_ => return Err(Error::MaintenanceRequired),
};
let response_schema = if kind == GateKind::Input {
Some(match node.configuration.get("input_resolution") {
Some(value) => {
let resolution = serde_json::from_value::<
bcode_workflow::WorkflowInputResolution,
>(value.clone())
.map_err(|_| Error::MaintenanceRequired)?;
if resolution.version != 1 || resolution.transform.output != node.output {
return Err(Error::MaintenanceRequired);
}
resolution
.transform
.validate()
.map_err(|_| Error::MaintenanceRequired)?;
jsonschema::validator_for(&resolution.supplied.schema)
.map_err(|_| Error::MaintenanceRequired)?;
resolution.supplied
}
None => node.output,
})
} else {
None
};
result.gates.push(GateWait {
node_id: activation.node_id,
activation_id: activation.activation_id,
requested_at_ms: activation.created_at_ms,
input: activation.input,
kind,
prompt,
response_schema,
});
}
Ok(result)
}