a3s-flow 1.1.0

Durable workflow engine and Rust SDK for A3S
Documentation
#![cfg(feature = "postgres")]

use a3s_flow::{
    migrate_postgres_flow, FlowEventStore, FlowTaskQueue, PostgresEventStore, PostgresFlowTaskQueue,
};
use a3s_orm::PostgresExecutor;
use uuid::Uuid;

fn postgres_url_from_env() -> Option<String> {
    std::env::var("A3S_FLOW_POSTGRES_URL")
        .ok()
        .filter(|url| !url.trim().is_empty())
}

fn schema_scoped_url(postgres_url: &str, schema: &str) -> String {
    let separator = if postgres_url.contains('?') { '&' } else { '?' };
    format!("{postgres_url}{separator}options=-csearch_path%3D{schema}")
}

#[tokio::test]
async fn dedicated_migrator_admits_store_and_queue_without_serving_ddl() {
    let Some(postgres_url) = postgres_url_from_env() else {
        eprintln!("skipping PostgreSQL schema admission test; set A3S_FLOW_POSTGRES_URL");
        return;
    };
    let schema = format!("flow_schema_admission_{}", Uuid::new_v4().simple());
    let base_executor = PostgresExecutor::connect_no_tls(&postgres_url, 2).unwrap();
    base_executor
        .connection()
        .await
        .unwrap()
        .batch_execute(&format!("CREATE SCHEMA {schema}"))
        .await
        .unwrap();

    let scoped_url = schema_scoped_url(&postgres_url, &schema);
    let scoped_executor = PostgresExecutor::connect_no_tls(&scoped_url, 2).unwrap();
    let admission_error = PostgresEventStore::from_executor_verified(scoped_executor.clone())
        .await
        .expect_err("an unmigrated serving schema must fail admission");
    assert!(admission_error
        .to_string()
        .contains("PostgreSQL Flow schema admission failed"));

    let ledger: Option<String> = scoped_executor
        .connection()
        .await
        .unwrap()
        .query_one("SELECT to_regclass('a3s_orm_migrations')::text", &[])
        .await
        .unwrap()
        .get(0);
    assert_eq!(ledger, None, "serving admission must not create the ledger");

    let report = migrate_postgres_flow(&scoped_executor).await.unwrap();
    assert!(!report.applied.is_empty());
    let store = PostgresEventStore::from_executor_verified(scoped_executor.clone())
        .await
        .unwrap();
    let queue = PostgresFlowTaskQueue::from_executor_verified_with_queue(
        scoped_executor.clone(),
        "admitted",
    )
    .await
    .unwrap();
    assert!(store.list_run_ids().await.unwrap().is_empty());
    assert_eq!(queue.len().await.unwrap(), 0);

    drop(queue);
    drop(store);
    drop(scoped_executor);
    base_executor
        .connection()
        .await
        .unwrap()
        .batch_execute(&format!("DROP SCHEMA {schema} CASCADE"))
        .await
        .unwrap();
}