Surface publish validation warnings on API responses

In lenient mode (FailOnValidationErrors = false), PublishAsync returns
Succeeded = true while ValidationErrors may still be non-empty. Previously the
publish API endpoints discarded those warnings, so lenient-mode callers got a
200 OK with no indication of the validation issues.

- Publish and Post (save-and-publish) responses now include a ValidationErrors
  collection populated with the publish result's validation messages.
- BulkPublish response now includes a Warnings dictionary mapping each
  successfully-published definition id to its validation warning messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Sipke Schoorstra 2026-06-19 21:29:00 +02:00
parent 354d59942c
commit 2863c980f3
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
6 changed files with 13 additions and 6 deletions

View file

@ -42,6 +42,7 @@ internal class BulkPublish(
var skipped = new List<string>();
var updatedConsumers = new List<string>();
var failed = new List<string>();
var warnings = new Dictionary<string, ICollection<string>>();
var publishableDefinitions = new List<(string DefinitionId, WorkflowDefinition Definition)>();
var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter
@ -97,12 +98,15 @@ internal class BulkPublish(
}
published.Add(definitionId);
if (result.ValidationErrors.Count > 0)
warnings[definitionId] = result.ValidationErrors.Select(x => x.Message).ToList();
if (result.AffectedWorkflows.WorkflowDefinitions.Count > 0)
updatedConsumers.AddRange(result.AffectedWorkflows.WorkflowDefinitions.Select(x => x.DefinitionId));
}
return new(published, alreadyPublished, notFound, skipped, updatedConsumers, failed);
return new(published, alreadyPublished, notFound, skipped, updatedConsumers, failed, warnings);
}
}

View file

@ -5,7 +5,7 @@ internal class Request
public ICollection<string> DefinitionIds { get; set; } = default!;
}
internal class Response(ICollection<string> published, ICollection<string> alreadyPublished, ICollection<string> notFound, ICollection<string> skipped, ICollection<string> updatedConsumers, ICollection<string> failed)
internal class Response(ICollection<string> published, ICollection<string> alreadyPublished, ICollection<string> notFound, ICollection<string> skipped, ICollection<string> updatedConsumers, ICollection<string> failed, IDictionary<string, ICollection<string>> warnings)
{
public ICollection<string> Published { get; } = published;
public ICollection<string> AlreadyPublished { get; } = alreadyPublished;
@ -13,4 +13,5 @@ internal class Response(ICollection<string> published, ICollection<string> alrea
public ICollection<string> Skipped { get; } = skipped;
public ICollection<string> UpdatedConsumers { get; } = updatedConsumers;
public ICollection<string> Failed { get; } = failed;
public IDictionary<string, ICollection<string>> Warnings { get; } = warnings;
}

View file

@ -116,7 +116,8 @@ internal class Post(
var mappedDefinition = await linker.MapAsync(draft, cancellationToken);
var affectedWorkflows = result?.AffectedWorkflows?.WorkflowDefinitions ?? [];
var response = new Response(mappedDefinition, false, affectedWorkflows.Count);
var validationErrors = result?.ValidationErrors.Select(e => e.Message).ToList() ?? [];
var response = new Response(mappedDefinition, false, affectedWorkflows.Count, validationErrors);
await HttpContext.Response.WriteAsJsonAsync(response, serializerOptions, cancellationToken);
}

View file

@ -2,4 +2,4 @@ using Elsa.Workflows.Api.Models;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Post;
internal record Response(LinkedWorkflowDefinitionModel WorkflowDefinition, bool AlreadyPublished, int ConsumingWorkflowCount);
internal record Response(LinkedWorkflowDefinitionModel WorkflowDefinition, bool AlreadyPublished, int ConsumingWorkflowCount, ICollection<string> ValidationErrors);

View file

@ -70,8 +70,9 @@ internal class Publish(
return;
}
var validationErrors = result?.ValidationErrors.Select(e => e.Message).ToList() ?? [];
var mappedDefinition = await linker.MapAsync(definition, cancellationToken);
var response = new Response(mappedDefinition, isPublished, result?.AffectedWorkflows.WorkflowDefinitions.Count ?? 0);
var response = new Response(mappedDefinition, isPublished, result?.AffectedWorkflows.WorkflowDefinitions.Count ?? 0, validationErrors);
await Send.OkAsync(response, cancellationToken);
}
}

View file

@ -7,4 +7,4 @@ internal class Request
public string DefinitionId { get; set; } = default!;
}
internal record Response(LinkedWorkflowDefinitionModel WorkflowDefinition, bool AlreadyPublished, int ConsumingWorkflowCount);
internal record Response(LinkedWorkflowDefinitionModel WorkflowDefinition, bool AlreadyPublished, int ConsumingWorkflowCount, ICollection<string> ValidationErrors);