feat(wfe-core): human-friendly workflow names
Add a `name` field to both `WorkflowDefinition` (optional display name
declared in YAML, e.g. "Continuous Integration") and `WorkflowInstance`
(required, unique alongside the UUID primary key). Instance names are
auto-assigned as `{definition_id}-{N}` via a per-definition monotonic
counter so the 42nd run of `ci` becomes `ci-42`.
Persistence trait gains two methods:
* `get_workflow_instance_by_name` — name-based lookup for Get/Cancel/
Suspend/Resume/Watch/Logs RPCs so callers can address instances
interchangeably as either UUID or human name.
* `next_definition_sequence` — atomic per-definition counter used by
the host at start time to allocate the next N.
This commit wires the in-memory test provider and touches the deno
bridge test helper; the real postgres/sqlite impls follow in the next
commit. UUIDs remain the primary key throughout — names are a second
unique index, never a replacement.
This commit is contained in:
@@ -9,7 +9,14 @@ use super::service::ServiceDefinition;
|
||||
/// A compiled workflow definition ready for execution.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkflowDefinition {
|
||||
/// Stable slug used as the primary key (e.g. "ci", "checkout"). Must be
|
||||
/// unique within a host. Referenced by other workflows, webhooks, and
|
||||
/// clients when starting new instances.
|
||||
pub id: String,
|
||||
/// Optional human-friendly display name surfaced in UIs, listings, and
|
||||
/// logs (e.g. "Continuous Integration"). Falls back to `id` when unset.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
pub version: u32,
|
||||
pub description: Option<String>,
|
||||
pub steps: Vec<WorkflowStep>,
|
||||
@@ -25,6 +32,7 @@ impl WorkflowDefinition {
|
||||
pub fn new(id: impl Into<String>, version: u32) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
name: None,
|
||||
version,
|
||||
description: None,
|
||||
steps: Vec::new(),
|
||||
@@ -33,6 +41,11 @@ impl WorkflowDefinition {
|
||||
services: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the display name when set, otherwise fall back to the slug id.
|
||||
pub fn display_name(&self) -> &str {
|
||||
self.name.as_deref().unwrap_or(&self.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A single step in a workflow definition.
|
||||
|
||||
@@ -6,7 +6,14 @@ use super::status::{PointerStatus, WorkflowStatus};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkflowInstance {
|
||||
/// UUID — the primary key, always unique, never changes.
|
||||
pub id: String,
|
||||
/// Human-friendly unique name, e.g. "ci-42". Auto-assigned as
|
||||
/// `{definition_id}-{N}` via a per-definition monotonic counter when
|
||||
/// the caller does not supply an override. Used interchangeably with
|
||||
/// `id` in lookup APIs. Empty when the instance has not yet been
|
||||
/// persisted (the host fills it in before the first insert).
|
||||
pub name: String,
|
||||
pub workflow_definition_id: String,
|
||||
pub version: u32,
|
||||
pub description: Option<String>,
|
||||
@@ -20,9 +27,15 @@ pub struct WorkflowInstance {
|
||||
}
|
||||
|
||||
impl WorkflowInstance {
|
||||
pub fn new(workflow_definition_id: impl Into<String>, version: u32, data: serde_json::Value) -> Self {
|
||||
pub fn new(
|
||||
workflow_definition_id: impl Into<String>,
|
||||
version: u32,
|
||||
data: serde_json::Value,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
// Filled in by WorkflowHost::start_workflow before persisting.
|
||||
name: String::new(),
|
||||
workflow_definition_id: workflow_definition_id.into(),
|
||||
version,
|
||||
description: None,
|
||||
@@ -134,7 +147,10 @@ mod tests {
|
||||
let json = serde_json::to_string(&instance).unwrap();
|
||||
let deserialized: WorkflowInstance = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(instance.id, deserialized.id);
|
||||
assert_eq!(instance.workflow_definition_id, deserialized.workflow_definition_id);
|
||||
assert_eq!(
|
||||
instance.workflow_definition_id,
|
||||
deserialized.workflow_definition_id
|
||||
);
|
||||
assert_eq!(instance.version, deserialized.version);
|
||||
assert_eq!(instance.status, deserialized.status);
|
||||
assert_eq!(instance.data, deserialized.data);
|
||||
|
||||
@@ -5,9 +5,7 @@ use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::models::{
|
||||
Event, EventSubscription, ExecutionError, ScheduledCommand, WorkflowInstance,
|
||||
};
|
||||
use crate::models::{Event, EventSubscription, ExecutionError, ScheduledCommand, WorkflowInstance};
|
||||
use crate::traits::{
|
||||
EventRepository, PersistenceProvider, ScheduledCommandRepository, SubscriptionRepository,
|
||||
WorkflowRepository,
|
||||
@@ -22,6 +20,9 @@ pub struct InMemoryPersistenceProvider {
|
||||
subscriptions: Arc<RwLock<HashMap<String, EventSubscription>>>,
|
||||
errors: Arc<RwLock<Vec<ExecutionError>>>,
|
||||
scheduled_commands: Arc<RwLock<Vec<ScheduledCommand>>>,
|
||||
/// Per-definition monotonic counter used to generate human-friendly
|
||||
/// workflow instance names of the form `{definition_id}-{N}`.
|
||||
sequences: Arc<RwLock<HashMap<String, u64>>>,
|
||||
}
|
||||
|
||||
impl InMemoryPersistenceProvider {
|
||||
@@ -32,6 +33,7 @@ impl InMemoryPersistenceProvider {
|
||||
subscriptions: Arc::new(RwLock::new(HashMap::new())),
|
||||
errors: Arc::new(RwLock::new(Vec::new())),
|
||||
scheduled_commands: Arc::new(RwLock::new(Vec::new())),
|
||||
sequences: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +109,23 @@ impl WorkflowRepository for InMemoryPersistenceProvider {
|
||||
.ok_or_else(|| WfeError::WorkflowNotFound(id.to_string()))
|
||||
}
|
||||
|
||||
async fn get_workflow_instance_by_name(&self, name: &str) -> Result<WorkflowInstance> {
|
||||
self.workflows
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.find(|w| w.name == name)
|
||||
.cloned()
|
||||
.ok_or_else(|| WfeError::WorkflowNotFound(name.to_string()))
|
||||
}
|
||||
|
||||
async fn next_definition_sequence(&self, definition_id: &str) -> Result<u64> {
|
||||
let mut seqs = self.sequences.write().await;
|
||||
let next = seqs.get(definition_id).copied().unwrap_or(0) + 1;
|
||||
seqs.insert(definition_id.to_string(), next);
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
async fn get_workflow_instances(&self, ids: &[String]) -> Result<Vec<WorkflowInstance>> {
|
||||
let workflows = self.workflows.read().await;
|
||||
let mut result = Vec::new();
|
||||
@@ -121,10 +140,7 @@ impl WorkflowRepository for InMemoryPersistenceProvider {
|
||||
|
||||
#[async_trait]
|
||||
impl SubscriptionRepository for InMemoryPersistenceProvider {
|
||||
async fn create_event_subscription(
|
||||
&self,
|
||||
subscription: &EventSubscription,
|
||||
) -> Result<String> {
|
||||
async fn create_event_subscription(&self, subscription: &EventSubscription) -> Result<String> {
|
||||
let id = if subscription.id.is_empty() {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
} else {
|
||||
@@ -217,11 +233,7 @@ impl SubscriptionRepository for InMemoryPersistenceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async fn clear_subscription_token(
|
||||
&self,
|
||||
subscription_id: &str,
|
||||
token: &str,
|
||||
) -> Result<()> {
|
||||
async fn clear_subscription_token(&self, subscription_id: &str, token: &str) -> Result<()> {
|
||||
let mut subs = self.subscriptions.write().await;
|
||||
match subs.get_mut(subscription_id) {
|
||||
Some(sub) => {
|
||||
@@ -282,7 +294,9 @@ impl EventRepository for InMemoryPersistenceProvider {
|
||||
let events = self.events.read().await;
|
||||
let ids = events
|
||||
.values()
|
||||
.filter(|e| e.event_name == event_name && e.event_key == event_key && e.event_time <= as_of)
|
||||
.filter(|e| {
|
||||
e.event_name == event_name && e.event_key == event_key && e.event_time <= as_of
|
||||
})
|
||||
.map(|e| e.id.clone())
|
||||
.collect();
|
||||
Ok(ids)
|
||||
@@ -325,9 +339,14 @@ impl ScheduledCommandRepository for InMemoryPersistenceProvider {
|
||||
async fn process_commands(
|
||||
&self,
|
||||
as_of: DateTime<Utc>,
|
||||
handler: &(dyn Fn(ScheduledCommand) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
|
||||
+ Send
|
||||
+ Sync),
|
||||
handler: &(
|
||||
dyn Fn(
|
||||
ScheduledCommand,
|
||||
)
|
||||
-> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
|
||||
+ Send
|
||||
+ Sync
|
||||
),
|
||||
) -> Result<()> {
|
||||
let as_of_millis = as_of.timestamp_millis();
|
||||
let due: Vec<ScheduledCommand> = {
|
||||
@@ -360,7 +379,7 @@ impl PersistenceProvider for InMemoryPersistenceProvider {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::models::{Event, EventSubscription, ExecutionError, ScheduledCommand, CommandName};
|
||||
use crate::models::{CommandName, Event, EventSubscription, ExecutionError, ScheduledCommand};
|
||||
use crate::traits::{
|
||||
EventRepository, PersistenceProvider, ScheduledCommandRepository, SubscriptionRepository,
|
||||
WorkflowRepository,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::models::{
|
||||
Event, EventSubscription, ExecutionError, ScheduledCommand, WorkflowInstance,
|
||||
};
|
||||
use crate::models::{Event, EventSubscription, ExecutionError, ScheduledCommand, WorkflowInstance};
|
||||
|
||||
/// Persistence for workflow instances.
|
||||
#[async_trait]
|
||||
@@ -17,7 +15,15 @@ pub trait WorkflowRepository: Send + Sync {
|
||||
) -> crate::Result<()>;
|
||||
async fn get_runnable_instances(&self, as_at: DateTime<Utc>) -> crate::Result<Vec<String>>;
|
||||
async fn get_workflow_instance(&self, id: &str) -> crate::Result<WorkflowInstance>;
|
||||
async fn get_workflow_instance_by_name(&self, name: &str)
|
||||
-> crate::Result<WorkflowInstance>;
|
||||
async fn get_workflow_instances(&self, ids: &[String]) -> crate::Result<Vec<WorkflowInstance>>;
|
||||
|
||||
/// Atomically allocate the next sequence number for a given workflow
|
||||
/// definition id. Used by the host to assign human-friendly names of the
|
||||
/// form `{definition_id}-{N}` before inserting a new workflow instance.
|
||||
/// Guaranteed monotonic per definition_id; no guarantees across definitions.
|
||||
async fn next_definition_sequence(&self, definition_id: &str) -> crate::Result<u64>;
|
||||
}
|
||||
|
||||
/// Persistence for event subscriptions.
|
||||
@@ -79,9 +85,14 @@ pub trait ScheduledCommandRepository: Send + Sync {
|
||||
async fn process_commands(
|
||||
&self,
|
||||
as_of: DateTime<Utc>,
|
||||
handler: &(dyn Fn(ScheduledCommand) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::Result<()>> + Send>>
|
||||
+ Send
|
||||
+ Sync),
|
||||
handler: &(
|
||||
dyn Fn(
|
||||
ScheduledCommand,
|
||||
) -> std::pin::Pin<
|
||||
Box<dyn std::future::Future<Output = crate::Result<()>> + Send>,
|
||||
> + Send
|
||||
+ Sync
|
||||
),
|
||||
) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use wfe_core::WfeError;
|
||||
use wfe_core::models::ExecutionResult;
|
||||
use wfe_core::traits::step::{StepBody, StepExecutionContext};
|
||||
use wfe_core::WfeError;
|
||||
|
||||
/// A request sent from the executor (tokio) to the V8 thread.
|
||||
pub struct StepRequest {
|
||||
@@ -160,7 +160,9 @@ pub fn deserialize_execution_result(
|
||||
value: &serde_json::Value,
|
||||
) -> wfe_core::Result<ExecutionResult> {
|
||||
let js_result: JsExecutionResult = serde_json::from_value(value.clone()).map_err(|e| {
|
||||
WfeError::StepExecution(format!("failed to deserialize ExecutionResult from JS: {e}"))
|
||||
WfeError::StepExecution(format!(
|
||||
"failed to deserialize ExecutionResult from JS: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(ExecutionResult {
|
||||
@@ -186,6 +188,7 @@ mod tests {
|
||||
fn make_test_context() -> (WorkflowInstance, WorkflowStep, ExecutionPointer) {
|
||||
let instance = WorkflowInstance {
|
||||
id: "wf-1".into(),
|
||||
name: "test-def-1".into(),
|
||||
workflow_definition_id: "test-def".into(),
|
||||
version: 1,
|
||||
description: None,
|
||||
@@ -373,7 +376,9 @@ mod tests {
|
||||
assert_eq!(req.step_type, "MyStep");
|
||||
assert_eq!(req.request_id, 0);
|
||||
req.response_tx
|
||||
.send(Ok(serde_json::json!({"proceed": true, "outputData": {"done": true}})))
|
||||
.send(Ok(
|
||||
serde_json::json!({"proceed": true, "outputData": {"done": true}}),
|
||||
))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user