use crate::errors::OrionError;
use serde_json::Value;
pub(crate) async fn ensure_connectors_exist(
connectors: &crate::connector::ConnectorRegistry,
workflow: &crate::storage::models::Workflow,
) -> Result<(), OrionError> {
let Ok(tasks) = serde_json::from_str::<Value>(&workflow.tasks_json) else {
return Ok(()); };
let mut facts: std::collections::HashMap<String, crate::engine::ConnectorFacts> =
std::collections::HashMap::new();
for r in crate::engine::connector_refs(&tasks) {
if facts.contains_key(r.connector) {
continue;
}
if let Some(config) = connectors.get(r.connector).await {
facts.insert(
r.connector.to_string(),
crate::engine::ConnectorFacts {
connector_type: config.connector_type(),
is_mongo: config.is_mongo(),
},
);
}
}
let mut missing: Vec<String> = Vec::new();
let mut problems: Vec<String> = Vec::new();
for problem in crate::engine::check_connector_refs(&tasks, |name| facts.get(name).copied()) {
match problem {
crate::engine::RefProblem::Missing { connector } => {
if !missing.iter().any(|m| m == connector) {
missing.push(connector.to_string());
}
}
crate::engine::RefProblem::WrongType {
function,
connector,
actual,
wanted,
} => problems.push(format!(
"task calling '{function}' points at connector '{connector}', which is a \
'{actual}' connector — '{function}' requires {}",
wanted
.iter()
.map(|t| format!("'{t}'"))
.collect::<Vec<_>>()
.join(" or ")
)),
crate::engine::RefProblem::MissingMongoDatabase {
function,
connector,
} => problems.push(format!(
"task calling '{function}' points at MongoDB connector '{connector}' but \
sets no 'database' — MongoDB connection strings carry no default database"
)),
}
}
if !missing.is_empty() {
return Err(OrionError::validation(format!(
"Cannot activate workflow '{}': connector(s) {} not found — create \
them first, or fix the reference",
workflow.workflow_id,
missing
.iter()
.map(|m| format!("'{m}'"))
.collect::<Vec<_>>()
.join(", ")
)));
}
if !problems.is_empty() {
return Err(OrionError::validation(format!(
"Cannot activate workflow '{}': {}",
workflow.workflow_id,
problems.join("; ")
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn workflow_with(tasks: serde_json::Value) -> crate::storage::models::Workflow {
let now = chrono::Utc::now().naive_utc();
crate::storage::models::Workflow {
workflow_id: "w".to_string(),
version: 1,
name: "w".to_string(),
description: None,
priority: 0,
status: "draft".to_string(),
rollout_percentage: 100,
condition_json: "true".to_string(),
tasks_json: tasks.to_string(),
tags_json: "[]".to_string(),
loop_json: None,
continue_on_error: false,
created_at: now,
updated_at: now,
}
}
fn empty_registry() -> crate::connector::ConnectorRegistry {
crate::connector::ConnectorRegistry::new(Default::default())
}
#[tokio::test]
async fn a_missing_connector_is_refused_without_going_through_http() {
let registry = empty_registry();
let wf = workflow_with(serde_json::json!([{
"id": "t1",
"name": "read",
"function": { "name": "db_read", "input": { "connector": "nope", "query": "SELECT 1" } }
}]));
let err = ensure_connectors_exist(®istry, &wf)
.await
.expect_err("a workflow naming a connector that does not exist must not activate");
assert!(
err.to_string().contains("nope"),
"the refusal must name the connector: {err}"
);
}
#[tokio::test]
async fn a_workflow_with_no_connector_refs_passes() {
let registry = empty_registry();
let wf = workflow_with(serde_json::json!([{
"id": "t1",
"name": "log",
"function": { "name": "log", "input": { "message": "hi" } }
}]));
assert!(ensure_connectors_exist(®istry, &wf).await.is_ok());
}
}