diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/IWorkflowActorRoleResolver.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/IWorkflowActorRoleResolver.cs new file mode 100644 index 000000000..7a9fd0f77 --- /dev/null +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/IWorkflowActorRoleResolver.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace StellaOps.Workflow.Abstractions; + +/// +/// Resolves the authenticated actor's identity (ID + roles) from the server-side +/// request context. Implementations may query a database, read JWT claims, or +/// fall back to client-supplied values. +/// +public interface IWorkflowActorRoleResolver +{ + /// + /// Returns the actor ID for the current request. + /// Implementations should prefer server-side identity over client-supplied values. + /// + string ResolveActorId(string? clientActorId); + + /// + /// Resolves user roles from a trusted source (database, identity provider). + /// Falls back to when no server-side context + /// is available (e.g., during signal pump processing). + /// + Task> ResolveActorRolesAsync( + string actorId, + IReadOnlyCollection? clientRoles, + CancellationToken cancellationToken = default); +} + +/// +/// Default no-op resolver that passes through client-supplied values. +/// Used when no database-backed role resolution is configured. +/// +public sealed class NullWorkflowActorRoleResolver : IWorkflowActorRoleResolver +{ + public string ResolveActorId(string? clientActorId) + => clientActorId ?? string.Empty; + + public Task> ResolveActorRolesAsync( + string actorId, + IReadOnlyCollection? clientRoles, + CancellationToken cancellationToken = default) + => Task.FromResult>(clientRoles ?? []); +} diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalDefinitionCompiler.Helpers.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalDefinitionCompiler.Helpers.cs index a39bde064..915e74b1e 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalDefinitionCompiler.Helpers.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalDefinitionCompiler.Helpers.cs @@ -179,8 +179,10 @@ public static partial class WorkflowCanonicalDefinitionCompiler }, ResultKey = resultKey, TimeoutSeconds = timeoutSeconds, - WhenFailure = failureHandlers?.HasFailureBranch == true ? BuildSequence(failureHandlers.WhenFailure) : null, - WhenTimeout = failureHandlers?.HasTimeoutBranch == true ? BuildSequence(failureHandlers.WhenTimeout) : null, + // Preserve empty handlers (0 steps) as empty sequences — they mean "ignore failure and continue". + // Only set to null when no handler was provided at all. + WhenFailure = failureHandlers is not null ? BuildSequence(failureHandlers.WhenFailure) : null, + WhenTimeout = failureHandlers is not null ? BuildSequence(failureHandlers.WhenTimeout) : null, }; } diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowDeclarativeAbstractions.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowDeclarativeAbstractions.cs index 7d373c17f..8ba98212b 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowDeclarativeAbstractions.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowDeclarativeAbstractions.cs @@ -726,6 +726,104 @@ public sealed class WorkflowFlowBuilder null); } + public WorkflowFlowBuilder Call( + string stepName, + Address address, + WorkflowExpressionDefinition payloadExpression, + Action> whenFailure, + Action>? whenTimeout = null) + { + return AddMicroserviceCall( + stepName, + address.MicroserviceName, + address.Command, + context => WorkflowCanonicalExpressionRuntime.Evaluate(payloadExpression, context), + payloadExpression, + null, + whenFailure, + whenTimeout); + } + + public WorkflowFlowBuilder Call( + string stepName, + Address address, + WorkflowExpressionDefinition payloadExpression, + WorkflowHandledBranchAction onFailure, + WorkflowHandledBranchAction onTimeout = WorkflowHandledBranchAction.None) + { + return AddMicroserviceCall( + stepName, + address.MicroserviceName, + address.Command, + context => WorkflowCanonicalExpressionRuntime.Evaluate(payloadExpression, context), + payloadExpression, + null, + null, + null, + onFailure, + onTimeout); + } + + public WorkflowFlowBuilder Call( + string stepName, + Address address, + WorkflowExpressionDefinition payloadExpression, + string? resultKey = null) + { + ArgumentNullException.ThrowIfNull(payloadExpression); + return AddMicroserviceCall( + stepName, + address.MicroserviceName, + address.Command, + context => WorkflowCanonicalExpressionRuntime.Evaluate(payloadExpression, context), + payloadExpression, + resultKey ?? stepName, + null, + null); + } + + public WorkflowFlowBuilder Call( + string stepName, + Address address, + WorkflowExpressionDefinition payloadExpression, + Action> whenFailure, + Action>? whenTimeout = null, + string? resultKey = null) + { + ArgumentNullException.ThrowIfNull(payloadExpression); + return AddMicroserviceCall( + stepName, + address.MicroserviceName, + address.Command, + context => WorkflowCanonicalExpressionRuntime.Evaluate(payloadExpression, context), + payloadExpression, + resultKey, + whenFailure, + whenTimeout); + } + + public WorkflowFlowBuilder Call( + string stepName, + Address address, + WorkflowExpressionDefinition payloadExpression, + WorkflowHandledBranchAction onFailure, + WorkflowHandledBranchAction onTimeout = WorkflowHandledBranchAction.None, + string? resultKey = null) + { + ArgumentNullException.ThrowIfNull(payloadExpression); + return AddMicroserviceCall( + stepName, + address.MicroserviceName, + address.Command, + context => WorkflowCanonicalExpressionRuntime.Evaluate(payloadExpression, context), + payloadExpression, + resultKey, + null, + null, + onFailure, + onTimeout); + } + public WorkflowFlowBuilder Call( string stepName, Address address, diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs index 77486a4f1..e0d8cd899 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs @@ -78,6 +78,9 @@ public static class WorkflowRegistrationServiceCollectionExtensions private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true, + // Allow floating-point JSON numbers (e.g., 201000256548.0) to deserialize into integer + // types (long?, int?). This is needed because the workflow state round-trips through + // JSON serialization which may add ".0" to integer values. NumberHandling = global::System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString, }; @@ -191,6 +194,9 @@ public static class WorkflowRegistrationServiceCollectionExtensions return new Dictionary(payload, StringComparer.OrdinalIgnoreCase); } + // Normalize decimal-valued integers (e.g., 201000256548.0 → 201000256548) + // before serialization. This is needed because the workflow state may store + // integer values as decimals after JSON round-trips. var normalized = NormalizePayloadNumbers(payload); var json = JsonSerializer.Serialize(normalized, SerializerOptions); return JsonSerializer.Deserialize(json, startRequestType, SerializerOptions) diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowStartContracts.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowStartContracts.cs index 8c4d3716e..bf9270c83 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowStartContracts.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowStartContracts.cs @@ -16,4 +16,6 @@ public sealed record StartWorkflowResponse public required string WorkflowName { get; init; } public required string WorkflowVersion { get; init; } public WorkflowBusinessReference? BusinessReference { get; init; } + public IReadOnlyCollection NextTasks { get; init; } = []; + public IDictionary WorkflowState { get; init; } = new Dictionary(); } diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs index 0df8f01ac..06e080f81 100644 --- a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs +++ b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs @@ -87,6 +87,9 @@ public sealed record WorkflowTaskCompleteResponse { public required string WorkflowTaskId { get; init; } public required bool Completed { get; init; } + public string? WorkflowInstanceId { get; init; } + public IReadOnlyCollection NextTasks { get; init; } = []; + public IDictionary WorkflowState { get; init; } = new Dictionary(); } public sealed record WorkflowTaskAssignRequest