use super::{Database, DatabaseValue, RepresentationError as Error};
use crate::{WorkflowExecutionAuthority, WorkflowExecutionSessionLink};
use serde::{Deserialize, Serialize};
use switchy_database::query::FilterableQuery;
mod generated;
mod proof;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirectContributorReference {
pub run_id: String,
pub consumer_activation_id: String,
pub graph_revision: u64,
pub edge_ordinal: u64,
pub output_id: String,
pub output_checksum_sha256: String,
pub dispatch_identity: String,
pub execution: WorkflowExecutionSessionLink,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status", content = "reference", rename_all = "snake_case")]
pub enum DirectContributorObservation {
Available(Box<DirectContributorReference>),
Unsupported,
ProvenanceUnavailable,
}
pub async fn direct_contributor(
db: &dyn Database,
owner: &WorkflowExecutionAuthority,
caller: &WorkflowExecutionSessionLink,
expected_revision: u64,
) -> Result<DirectContributorObservation, Error> {
crate::validate_execution_session_link(caller).map_err(|_| Error::InvalidAdmission)?;
let tx = db.begin_transaction().await?;
super::execution_graph::verify_execution(&*tx, owner, caller).await?;
let revision = super::graph_pages::revision(&*tx, &caller.run_id).await?;
let result = if revision != expected_revision || revision != super::graph::INITIAL_REVISION {
DirectContributorObservation::ProvenanceUnavailable
} else {
proof::prove(&*tx, caller, revision).await?
};
tx.commit().await?;
Ok(result)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeneratedContributorPage {
pub mutation_id: String,
pub source_activation_id: String,
pub contributors: Vec<DirectContributorReference>,
pub next_node_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GeneratedContributorObservation {
Available(Box<GeneratedContributorPage>),
Unsupported,
ProvenanceUnavailable,
}
pub async fn generated_contributors(
db: &dyn Database,
owner: &WorkflowExecutionAuthority,
caller: &WorkflowExecutionSessionLink,
expected_revision: u64,
after_node_id: Option<&str>,
limit: u16,
) -> Result<GeneratedContributorObservation, Error> {
if !(1..=100).contains(&limit) {
return Err(Error::InvalidAdmission);
}
if let Some(cursor) = after_node_id {
crate::validate_id("contributor cursor", cursor).map_err(|_| Error::InvalidAdmission)?;
}
crate::validate_execution_session_link(caller).map_err(|_| Error::InvalidAdmission)?;
let tx = db.begin_transaction().await?;
super::execution_graph::verify_execution(&*tx, owner, caller).await?;
if super::safety::cancellation_requested_at(&*tx, &caller.run_id)
.await?
.is_some()
{
return Err(Error::InvalidAdmission);
}
let revision = super::graph_pages::revision(&*tx, &caller.run_id).await?;
let result = if revision != expected_revision || revision != 2 {
GeneratedContributorObservation::ProvenanceUnavailable
} else {
generated::prove(&*tx, caller, after_node_id, limit).await?
};
tx.commit().await?;
Ok(result)
}
pub(super) fn decode<T: serde::de::DeserializeOwned>(
row: &switchy_database::Row,
field: &str,
) -> Result<T, Error> {
match row.get(field) {
Some(DatabaseValue::String(json)) if json.len() <= super::MAX_ADMISSION_BYTES => {
serde_json::from_str(&json).map_err(|_| Error::MaintenanceRequired)
}
_ => Err(Error::MaintenanceRequired),
}
}
pub(super) async fn current_activation(
db: &dyn Database,
run: &str,
node: &str,
) -> Result<Option<switchy_database::Row>, Error> {
let mut rows = db
.select("workflow_activations_v2")
.where_eq("run_id", run)
.where_eq("node_id", node)
.where_eq("current", true)
.limit(2)
.execute(db)
.await?;
if rows.len() > 1 {
return Err(Error::MaintenanceRequired);
}
Ok(rows.pop())
}
pub(super) fn text(row: &switchy_database::Row, field: &str) -> Result<String, Error> {
match row.get(field) {
Some(DatabaseValue::String(value)) if value.len() <= super::MAX_ADMISSION_BYTES => {
Ok(value)
}
_ => Err(Error::MaintenanceRequired),
}
}