Global read-only and read-only workflows (#5406)

* Implemented global readonly and workflow readonly

* Fixed bug of unfilled versions

* Fixed failing test

* Updated pipeline

* Revert

* Pushed fixes based on the PR comments and fixed history bug

* Implemented changes based on PR comments

* Fixed issue in caused by AuthorizationHandler not being able to receive null resources

* Remove unused Google.Protobuf dependency in Program.cs

An unused Protobuf library import was found in Program.cs file. In order to clean up the code and improve readability, this import statement has been removed. No other changes have been made.

* Refactor code to reduce redundancy and improve readability

Optimize code by removing unnecessary constructor assignments and simplify function calls. The simplification is mainly achieved by directly passing the required services as constructor parameters instead of assigning them to private fields first. This results in a more readable and less redundant code.

---------

Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
This commit is contained in:
MariusVuscanNx 2024-05-27 11:06:39 +03:00 committed by GitHub
parent f03c62a7a4
commit d817d7ccaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
74 changed files with 812 additions and 529 deletions

View file

@ -41,12 +41,13 @@ const bool useDapper = false;
const bool useProtoActor = false;
const bool useHangfire = false;
const bool useQuartz = true;
const bool useMassTransit = true;
const bool useMassTransit = false;
const bool useZipCompression = true;
const bool runEFCoreMigrations = true;
const bool useMemoryStores = false;
const bool useCaching = true;
const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit;
const bool useCaching = false;
const bool useReadOnlyMode = false;
const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.None;
const MassTransitBroker useMassTransitBroker = MassTransitBroker.Memory;
var builder = WebApplication.CreateBuilder(args);
@ -160,6 +161,7 @@ services
management.UseCache();
management.SetDefaultLogPersistenceMode(LogPersistenceMode.Inherit);
management.UseReadOnlyMode(useReadOnlyMode);
})
.UseWorkflowRuntime(runtime =>
{
@ -242,7 +244,10 @@ services
if (useQuartz)
scheduling.UseQuartzScheduler();
})
.UseWorkflowsApi(api => api.AddFastEndpointsAssembly<Program>())
.UseWorkflowsApi(api =>
{
api.AddFastEndpointsAssembly<Program>();
})
.UseRealTimeWorkflows()
.UseCSharp(options =>
{

View file

@ -4,9 +4,9 @@ using Elsa.Api.Client.Shared.Models;
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
/// <summary>
/// Represents a versioned workflow definition.
/// Represents a linked workflow definition.
/// </summary>
public class WorkflowDefinition : VersionedEntity
public class WorkflowDefinition : LinkedEntity
{
/// <summary>
/// The logical ID of the workflow. This ID is the same across versions.
@ -77,7 +77,7 @@ public class WorkflowDefinition : VersionedEntity
/// The root activity of the workflow.
/// </summary>
public JsonObject Root { get; set; } = default!;
/// <summary>
/// An option to use the workflow as a readonly workflow.
/// </summary>

View file

@ -1,4 +1,5 @@
using System.Text.Json.Nodes;
using Elsa.Api.Client.Shared.Models;
using JetBrains.Annotations;
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
@ -7,7 +8,7 @@ namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
/// Represents a serializable workflow definition.
/// </summary>
[PublicAPI]
public class WorkflowDefinitionModel
public class WorkflowDefinitionModel : LinkedEntity
{
/// <summary>
/// Gets or sets the version ID of the workflow definition.

View file

@ -1,3 +1,4 @@
using Elsa.Api.Client.Shared.Models;
using JetBrains.Annotations;
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
@ -6,4 +7,15 @@ namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
/// A summary of a workflow definition.
/// </summary>
[PublicAPI]
public record WorkflowDefinitionSummary(string Id, string DefinitionId, string Name, string? Description, int Version, bool IsLatest, bool IsPublished, string MaterializerName, DateTimeOffset CreatedAt);
public class WorkflowDefinitionSummary : LinkedEntity
{
public string Id { get; set; }
public string DefinitionId { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
public int Version { get; set; }
public bool IsLatest { get; set; }
public bool IsPublished { get; set; }
public string MaterializerName { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}

View file

@ -0,0 +1,3 @@
namespace Elsa.Api.Client.Shared.Models;
public record Link(string Href, string Rel, string Method);

View file

@ -0,0 +1,12 @@
namespace Elsa.Api.Client.Shared.Models;
/// <summary>
/// Represents an entity that is linked, meaning it contains links used in the context of HATEOAS.
/// </summary>
public class LinkedEntity : VersionedEntity
{
/// <summary>
/// A list of links that with the possible actions used in the context of HATEOAS.
/// </summary>
public Link[]? Links { get; set; } = default;
}

View file

@ -6,4 +6,8 @@ namespace Elsa.Api.Client.Shared.Models;
/// <param name="Items">A page of items.</param>
/// <param name="TotalCount">The total number of items.</param>
/// <typeparam name="T">The type of the items.</typeparam>
public record PagedListResponse<T>(ICollection<T> Items, long TotalCount);
public class PagedListResponse<T> : LinkedEntity
{
public ICollection<T> Items { get; set; }
public long TotalCount { get; set; }
}

View file

@ -0,0 +1,8 @@
namespace Elsa.Models;
public record LinkedResource(Link[] Links)
{
public LinkedResource() : this([]) { }
}
public record Link(string Href, string Rel, string Method);

View file

@ -2,7 +2,7 @@ using Elsa.Common.Models;
namespace Elsa.Models;
public class PagedListResponse<T>
public record PagedListResponse<T>: LinkedResource
{
public PagedListResponse()
{

View file

@ -176,6 +176,8 @@ public class DapperWorkflowDefinitionStore : IWorkflowDefinitionStore
.Is(nameof(WorkflowDefinition.Name), filter.Name)
.In(nameof(WorkflowDefinition.Name), filter.Names)
.Is(nameof(WorkflowDefinition.Options.UsableAsActivity), filter.UsableAsActivity)
.Is(nameof(WorkflowDefinition.IsSystem), filter.IsSystem)
.Is(nameof(WorkflowDefinition.IsReadonly), filter.IsReadonly)
.WorkflowDefinitionSearchTerm(filter.SearchTerm);
}

View file

@ -194,6 +194,7 @@ public class EFCoreWorkflowDefinitionStore : IWorkflowDefinitionStore
if (filter.UsableAsActivity != null) queryable = queryable.Where(x => EF.Property<bool>(x, "UsableAsActivity") == filter.UsableAsActivity);
if (!string.IsNullOrWhiteSpace(filter.SearchTerm)) queryable = queryable.Where(x => x.Name!.Contains(filter.SearchTerm) || x.Description!.Contains(filter.SearchTerm) || x.Id.Contains(filter.SearchTerm) || x.DefinitionId.Contains(filter.SearchTerm));
if (filter.IsSystem != null) queryable = queryable.Where(x => x.IsSystem == filter.IsSystem);
if (filter.IsReadonly != null) queryable = queryable.Where(x => x.IsReadonly == filter.IsReadonly);
return queryable;
}

View file

@ -0,0 +1,9 @@
namespace Elsa.Workflows.Api.Constants;
public static class AuthorizationPolicies
{
/// <summary>
/// The read-only policy ensures that that workflow management is not possible when the read-only mode is enabled or when a workflow is marked as read-only.
/// </summary>
public const string NotReadOnlyPolicy = "NotReadOnlyPolicy";
}

View file

@ -0,0 +1,19 @@
using Elsa.Models;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Api;
/// Maps workflow definition models to liked models
public interface IWorkflowDefinitionLinker
{
/// Maps to an enhanced model that contains links with the possible operations applicable to a workflow definition.
Task<LinkedWorkflowDefinitionModel> MapAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default);
/// Maps a paged list to an enhanced model that contains links with the possible operations applicable to a workflow definition.
PagedListResponse<LinkedWorkflowDefinitionSummary> MapAsync(PagedListResponse<WorkflowDefinitionSummary> list, CancellationToken cancellationToken = default);
/// Maps a list to an enhanced model that contains links with the possible operations applicable to a workflow definition.
Task<List<LinkedWorkflowDefinitionModel>> MapAsync(List<WorkflowDefinition> definitions, CancellationToken cancellationToken = default);
}

View file

@ -8,11 +8,11 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj"/>
<ProjectReference Include="..\Elsa.Http\Elsa.Http.csproj"/>
<ProjectReference Include="..\Elsa.JavaScript\Elsa.JavaScript.csproj"/>
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj"/>
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj"/>
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj" />
<ProjectReference Include="..\Elsa.Http\Elsa.Http.csproj" />
<ProjectReference Include="..\Elsa.JavaScript\Elsa.JavaScript.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
</Project>

View file

@ -13,12 +13,7 @@ internal class Request
}
[PublicAPI]
internal class Response
internal class Response(IDictionary<string, object> items)
{
public Response(IDictionary<string,object> items)
{
Items = items;
}
public IDictionary<string, object> Items { get; set; }
public IDictionary<string, object> Items { get; set; } = items;
}

View file

@ -5,15 +5,8 @@ using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityDescriptors.List;
[PublicAPI]
internal class List : ElsaEndpointWithoutRequest<Response>
internal class List(IActivityRegistry registry) : ElsaEndpointWithoutRequest<Response>
{
private readonly IActivityRegistry _registry;
public List(IActivityRegistry registry)
{
_registry = registry;
}
public override void Configure()
{
Get("/descriptors/activities");
@ -22,7 +15,7 @@ internal class List : ElsaEndpointWithoutRequest<Response>
public override Task<Response> ExecuteAsync(CancellationToken cancellationToken)
{
var descriptors = _registry.ListAll().ToList();
var descriptors = registry.ListAll().ToList();
var response = new Response(descriptors);
return Task.FromResult(response);

View file

@ -4,13 +4,8 @@ using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityDescriptors.List;
[PublicAPI]
internal class Response
internal class Response(ICollection<ActivityDescriptor> items)
{
public Response(ICollection<ActivityDescriptor> items)
{
Items = items;
}
public ICollection<ActivityDescriptor> Items { get; set; }
public ICollection<ActivityDescriptor> Items { get; set; } = items;
public int Count => Items.Count;
}

View file

@ -12,15 +12,8 @@ namespace Elsa.Workflows.Api.Endpoints.IncidentStrategies.List;
/// <summary>
/// Returns list of available <see cref="IIncidentStrategy" /> implementations.
/// </summary>
internal class List : ElsaEndpointWithoutRequest<ListResponse<IncidentStrategyDescriptor>>
internal class List(IEnumerable<IIncidentStrategy> strategies) : ElsaEndpointWithoutRequest<ListResponse<IncidentStrategyDescriptor>>
{
private readonly IEnumerable<IIncidentStrategy> _strategies;
public List(IEnumerable<IIncidentStrategy> strategies)
{
_strategies = strategies;
}
public override void Configure()
{
Get("/descriptors/incident-strategies");
@ -29,7 +22,7 @@ internal class List : ElsaEndpointWithoutRequest<ListResponse<IncidentStrategyDe
public override Task<ListResponse<IncidentStrategyDescriptor>> ExecuteAsync(CancellationToken cancellationToken)
{
var descriptors = _strategies.Select(IncidentStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
var descriptors = strategies.Select(IncidentStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
var response =new ListResponse<IncidentStrategyDescriptor>(descriptors);
return Task.FromResult(response);
}

View file

@ -10,15 +10,8 @@ namespace Elsa.Workflows.Api.Endpoints.Scripting.ExpressionDescriptors.List;
/// Returns a TypeScript definition that is used by the Monaco editor to display intellisense for JavaScript expressions.
/// </summary>
[UsedImplicitly]
internal class List : ElsaEndpointWithoutRequest<ListResponse<ExpressionDescriptorModel>>
internal class List(IExpressionDescriptorRegistry expressionDescriptorRegistry) : ElsaEndpointWithoutRequest<ListResponse<ExpressionDescriptorModel>>
{
private readonly IExpressionDescriptorRegistry _expressionDescriptorRegistry;
public List(IExpressionDescriptorRegistry expressionDescriptorRegistry)
{
_expressionDescriptorRegistry = expressionDescriptorRegistry;
}
/// <inheritdoc />
public override void Configure()
{
@ -29,7 +22,7 @@ internal class List : ElsaEndpointWithoutRequest<ListResponse<ExpressionDescript
/// <inheritdoc />
public override Task HandleAsync(CancellationToken cancellationToken)
{
var descriptors = _expressionDescriptorRegistry.ListAll().ToList();
var descriptors = expressionDescriptorRegistry.ListAll().ToList();
var models = Map(descriptors).ToList();
var response = new ListResponse<ExpressionDescriptorModel>(models);
return SendOkAsync(response, cancellationToken);

View file

@ -1,13 +1,8 @@
namespace Elsa.Workflows.Api.Endpoints.StorageDrivers.List;
public class Response
public class Response(ICollection<StorageDriverDescriptor> items)
{
public Response(ICollection<StorageDriverDescriptor> items)
{
Items = items;
}
public ICollection<StorageDriverDescriptor> Items { get; set; }
public ICollection<StorageDriverDescriptor> Items { get; set; } = items;
}
public record StorageDriverDescriptor(string TypeName, string DisplayName);

View file

@ -1,13 +1,8 @@
namespace Elsa.Workflows.Api.Endpoints.VariableTypes.List;
internal class Response
internal class Response(ICollection<VariableTypeDescriptor> items)
{
public Response(ICollection<VariableTypeDescriptor> items)
{
Items = items;
}
public ICollection<VariableTypeDescriptor> Items { get; set; }
public ICollection<VariableTypeDescriptor> Items { get; set; } = items;
}
internal record VariableTypeDescriptor(string TypeName, string DisplayName, string Category, string? Description);

View file

@ -12,15 +12,8 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowActivationStrategies.List;
/// <summary>
/// Returns list of available <see cref="IWorkflowActivationStrategy" /> implementations.
/// </summary>
internal class List : ElsaEndpointWithoutRequest<ListResponse<WorkflowActivationStrategyDescriptor>>
internal class List(IEnumerable<IWorkflowActivationStrategy> strategies) : ElsaEndpointWithoutRequest<ListResponse<WorkflowActivationStrategyDescriptor>>
{
private readonly IEnumerable<IWorkflowActivationStrategy> _strategies;
public List(IEnumerable<IWorkflowActivationStrategy> strategies)
{
_strategies = strategies;
}
public override void Configure()
{
Get("/descriptors/workflow-activation-strategies");
@ -29,7 +22,7 @@ internal class List : ElsaEndpointWithoutRequest<ListResponse<WorkflowActivation
public override Task<ListResponse<WorkflowActivationStrategyDescriptor>> ExecuteAsync(CancellationToken cancellationToken)
{
var descriptors = _strategies.Select(WorkflowActivationStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
var descriptors = strategies.Select(WorkflowActivationStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
var response =new ListResponse<WorkflowActivationStrategyDescriptor>(descriptors);
return Task.FromResult(response);
}

View file

@ -1,19 +1,15 @@
using Elsa.Abstractions;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Management.Contracts;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Elsa.Workflows.Api.Requirements;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDelete;
[UsedImplicitly]
internal class BulkDelete : ElsaEndpoint<Request, Response>
internal class BulkDelete(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
public BulkDelete(IWorkflowDefinitionManager workflowDefinitionManager)
{
_workflowDefinitionManager = workflowDefinitionManager;
}
public override void Configure()
{
Post("/bulk-actions/delete/workflow-definitions/by-definition-id");
@ -22,7 +18,15 @@ internal class BulkDelete : ElsaEndpoint<Request, Response>
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var count = await _workflowDefinitionManager.BulkDeleteByDefinitionIdsAsync(request.DefinitionIds, cancellationToken);
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return null!;
}
var count = await workflowDefinitionManager.BulkDeleteByDefinitionIdsAsync(request.DefinitionIds, cancellationToken);
return new Response(count);
}
}

View file

@ -7,12 +7,7 @@ internal class Request
public ICollection<string> DefinitionIds { get; set; } = default!;
}
internal class Response
internal class Response(long deletedCount)
{
public Response(long deletedCount)
{
DeletedCount = deletedCount;
}
[JsonPropertyName("deleted")] public long DeletedCount { get; }
[JsonPropertyName("deleted")] public long DeletedCount { get; } = deletedCount;
}

View file

@ -1,19 +1,16 @@
using Elsa.Abstractions;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management.Contracts;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDeleteVersions;
[PublicAPI]
internal class BulkDeleteVersions : ElsaEndpoint<Request, Response>
internal class BulkDeleteVersions(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
public BulkDeleteVersions(IWorkflowDefinitionManager workflowDefinitionManager)
{
_workflowDefinitionManager = workflowDefinitionManager;
}
public override void Configure()
{
Post("/bulk-actions/delete/workflow-definitions/by-id");
@ -22,7 +19,15 @@ internal class BulkDeleteVersions : ElsaEndpoint<Request, Response>
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var count = await _workflowDefinitionManager.BulkDeleteByIdsAsync(request.Ids, cancellationToken);
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return null!;
}
var count = await workflowDefinitionManager.BulkDeleteByIdsAsync(request.Ids, cancellationToken);
return new Response(count);
}
}

View file

@ -7,12 +7,7 @@ internal class Request
public ICollection<string> Ids { get; set; } = default!;
}
internal class Response
internal class Response(long deletedCount)
{
public Response(long deletedCount)
{
DeletedCount = deletedCount;
}
[JsonPropertyName("deleted")] public long DeletedCount { get; }
[JsonPropertyName("deleted")] public long DeletedCount { get; } = deletedCount;
}

View file

@ -10,19 +10,9 @@ using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDispatch;
[PublicAPI]
internal class Endpoint : ElsaEndpoint<Request, Response>
internal class Endpoint(IWorkflowDefinitionStore store, IWorkflowDispatcher workflowDispatcher, IIdentityGenerator identityGenerator)
: ElsaEndpoint<Request, Response>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IWorkflowDispatcher _workflowDispatcher;
private readonly IIdentityGenerator _identityGenerator;
public Endpoint(IWorkflowDefinitionStore store, IWorkflowDispatcher workflowDispatcher, IIdentityGenerator identityGenerator)
{
_store = store;
_workflowDispatcher = workflowDispatcher;
_identityGenerator = identityGenerator;
}
public override void Configure()
{
Post("/workflow-definitions/{definitionId}/bulk-dispatch");
@ -34,7 +24,7 @@ internal class Endpoint : ElsaEndpoint<Request, Response>
var definitionId = request.DefinitionId;
var versionOptions = request.VersionOptions ?? VersionOptions.Published;
var exists = await _store.AnyAsync(
var exists = await store.AnyAsync(
new WorkflowDefinitionFilter
{
DefinitionId = definitionId,
@ -52,7 +42,7 @@ internal class Endpoint : ElsaEndpoint<Request, Response>
for (var i = 0; i < request.Count; i++)
{
var instanceId = _identityGenerator.GenerateId();
var instanceId = identityGenerator.GenerateId();
var triggerActivityId = request.TriggerActivityId;
var input = (IDictionary<string, object>?)request.Input;
var dispatchRequest = new DispatchWorkflowDefinitionRequest
@ -64,7 +54,7 @@ internal class Endpoint : ElsaEndpoint<Request, Response>
TriggerActivityId = triggerActivityId
};
await _workflowDispatcher.DispatchAsync(dispatchRequest, cancellationToken: cancellationToken);
await workflowDispatcher.DispatchAsync(dispatchRequest, cancellationToken: cancellationToken);
instanceIds.Add(instanceId);
}

View file

@ -1,23 +1,18 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkPublish;
[PublicAPI]
internal class BulkPublish : ElsaEndpoint<Request, Response>
internal class BulkPublish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
public BulkPublish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher)
{
_store = store;
_workflowDefinitionPublisher = workflowDefinitionPublisher;
}
public override void Configure()
{
Post("/bulk-actions/publish/workflow-definitions/by-definition-ids");
@ -26,20 +21,30 @@ internal class BulkPublish : ElsaEndpoint<Request, Response>
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return null!;
}
var published = new List<string>();
var notFound = new List<string>();
var alreadyPublished = new List<string>();
var skipped = new List<string>();
var definitions = (await _store.FindManyAsync(new WorkflowDefinitionFilter
{
DefinitionIds = request.DefinitionIds, VersionOptions = VersionOptions.Latest
}, cancellationToken: cancellationToken))
var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter
{
DefinitionIds = request.DefinitionIds,
VersionOptions = VersionOptions.Latest
}, cancellationToken: cancellationToken))
.DistinctBy(x => x.DefinitionId)
.ToDictionary(x => x.DefinitionId);
foreach (var definitionId in request.DefinitionIds)
{
if(!definitions.TryGetValue(definitionId, out var definition))
if (!definitions.TryGetValue(definitionId, out var definition))
{
notFound.Add(definitionId);
continue;
@ -51,10 +56,16 @@ internal class BulkPublish : ElsaEndpoint<Request, Response>
continue;
}
await _workflowDefinitionPublisher.PublishAsync(definition, cancellationToken);
if (definition.IsReadonly)
{
skipped.Add(definitionId);
continue;
}
await workflowDefinitionPublisher.PublishAsync(definition, cancellationToken);
published.Add(definitionId);
}
return new Response(published, alreadyPublished, notFound);
return new Response(published, alreadyPublished, notFound, skipped);
}
}

View file

@ -5,16 +5,10 @@ internal class Request
public ICollection<string> DefinitionIds { get; set; } = default!;
}
internal class Response
internal class Response(ICollection<string> published, ICollection<string> alreadyPublished, ICollection<string> notFound, ICollection<string> skipped)
{
public Response(ICollection<string> published, ICollection<string> alreadyPublished, ICollection<string> notFound)
{
Published = published;
AlreadyPublished = alreadyPublished;
NotFound = notFound;
}
public ICollection<string> Published { get; }
public ICollection<string> AlreadyPublished { get; }
public ICollection<string> NotFound { get; }
public ICollection<string> Published { get; } = published;
public ICollection<string> AlreadyPublished { get; } = alreadyPublished;
public ICollection<string> NotFound { get; } = notFound;
public ICollection<string> Skipped { get; } = skipped;
}

View file

@ -1,23 +1,18 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkRetract;
[PublicAPI]
internal class BulkRetract : ElsaEndpoint<Request, Response>
internal class BulkRetract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
public BulkRetract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher)
{
_store = store;
_workflowDefinitionPublisher = workflowDefinitionPublisher;
}
public override void Configure()
{
Post("/bulk-actions/retract/workflow-definitions/by-definition-ids");
@ -26,13 +21,22 @@ internal class BulkRetract : ElsaEndpoint<Request, Response>
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return null!;
}
var retracted = new List<string>();
var notFound = new List<string>();
var notPublished = new List<string>();
var skipped = new List<string>();
foreach (var definitionId in request.DefinitionIds)
{
var definitions = (await _store.FindManyAsync(new WorkflowDefinitionFilter
var definitions = (await store.FindManyAsync(new WorkflowDefinitionFilter
{
DefinitionId = definitionId,
VersionOptions = VersionOptions.LatestOrPublished
@ -51,10 +55,16 @@ internal class BulkRetract : ElsaEndpoint<Request, Response>
continue;
}
await _workflowDefinitionPublisher.RetractAsync(published, cancellationToken);
if (published.IsReadonly)
{
skipped.Add(definitionId);
continue;
}
await workflowDefinitionPublisher.RetractAsync(published, cancellationToken);
retracted.Add(definitionId);
}
return new Response(retracted, notPublished, notFound);
return new Response(retracted, notPublished, notFound, skipped);
}
}

View file

@ -5,21 +5,14 @@ internal class Request
public ICollection<string> DefinitionIds { get; set; } = default!;
}
internal class Response
internal class Response(ICollection<string> retracted, ICollection<string> alreadyRetracted, ICollection<string> notFound, ICollection<string> skipped)
{
public Response(ICollection<string> retracted, ICollection<string> alreadyRetracted, ICollection<string> notFound)
{
Retracted = retracted;
AlreadyRetracted = alreadyRetracted;
NotPublished = alreadyRetracted;
NotFound = notFound;
}
public ICollection<string> Retracted { get; }
public ICollection<string> AlreadyRetracted { get; }
public ICollection<string> Retracted { get; } = retracted;
public ICollection<string> AlreadyRetracted { get; } = alreadyRetracted;
public ICollection<string> Skipped { get; } = skipped;
[Obsolete("Use AlreadyRetracted instead")]
public ICollection<string> NotPublished { get; }
public ICollection<string> NotPublished { get; } = alreadyRetracted;
public ICollection<string> NotFound { get; }
public ICollection<string> NotFound { get; } = notFound;
}

View file

@ -8,15 +8,8 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Count;
/// An endpoint for counting workflow definitions.
/// </summary>
[PublicAPI]
internal class Count : ElsaEndpointWithoutRequest<Response>
internal class Count(IWorkflowDefinitionStore store) : ElsaEndpointWithoutRequest<Response>
{
private readonly IWorkflowDefinitionStore _store;
public Count(IWorkflowDefinitionStore store)
{
_store = store;
}
public override void Configure()
{
Get("/workflow-definitions/query/count");
@ -25,7 +18,7 @@ internal class Count : ElsaEndpointWithoutRequest<Response>
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var count = await _store.CountDistinctAsync(cancellationToken);
var count = await store.CountDistinctAsync(cancellationToken);
var response = new Response(count);
await SendOkAsync(response, cancellationToken);
}

View file

@ -1,17 +1,16 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Delete;
internal class Delete : ElsaEndpoint<Request>
internal class Delete(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService, IWorkflowDefinitionStore store)
: ElsaEndpoint<Request>
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
public Delete(IWorkflowDefinitionManager workflowDefinitionManager)
{
_workflowDefinitionManager = workflowDefinitionManager;
}
public override void Configure()
{
Delete("/workflow-definitions/{definitionId}");
@ -20,7 +19,29 @@ internal class Delete : ElsaEndpoint<Request>
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var result = await _workflowDefinitionManager.DeleteByDefinitionIdAsync(request.DefinitionId, cancellationToken);
var filter = new WorkflowDefinitionFilter
{
DefinitionId = request.DefinitionId,
VersionOptions = VersionOptions.Latest
};
var definition = await store.FindAsync(filter, cancellationToken);
if (definition == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var result = await workflowDefinitionManager.DeleteByDefinitionIdAsync(request.DefinitionId, cancellationToken);
if (result == 0)
await SendNotFoundAsync(cancellationToken);

View file

@ -1,19 +1,16 @@
using Elsa.Abstractions;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management.Contracts;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.DeleteVersion;
[PublicAPI]
internal class DeleteVersion : ElsaEndpoint<Request>
internal class DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService)
: ElsaEndpoint<Request>
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
public DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager)
{
_workflowDefinitionManager = workflowDefinitionManager;
}
public override void Configure()
{
Delete("/workflow-definition-versions/{id}");
@ -22,7 +19,15 @@ internal class DeleteVersion : ElsaEndpoint<Request>
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var deleted = await _workflowDefinitionManager.DeleteByIdAsync(request.Id, cancellationToken);
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var deleted = await workflowDefinitionManager.DeleteByIdAsync(request.Id, cancellationToken);
if (!deleted)
await SendNotFoundAsync(cancellationToken);

View file

@ -6,7 +6,6 @@ using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Options;
using Elsa.Workflows.Runtime.Parameters;
using Elsa.Workflows.State;
using JetBrains.Annotations;

View file

@ -17,12 +17,7 @@ public class Request
public object? Input { get; set; }
}
public class Response
public class Response(WorkflowState workflowState)
{
public Response(WorkflowState workflowState)
{
WorkflowState = workflowState;
}
public WorkflowState WorkflowState { get; }
public WorkflowState WorkflowState { get; } = workflowState;
}

View file

@ -5,7 +5,6 @@ using Elsa.Extensions;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Serialization.Converters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
@ -13,19 +12,9 @@ using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetByDefinitionId;
[PublicAPI]
internal class GetByDefinitionId : ElsaEndpoint<Request>
internal class GetByDefinitionId(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, IWorkflowDefinitionLinker linker)
: ElsaEndpoint<Request>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IApiSerializer _apiSerializer;
private readonly WorkflowDefinitionMapper _mapper;
public GetByDefinitionId(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, WorkflowDefinitionMapper mapper)
{
_store = store;
_apiSerializer = apiSerializer;
_mapper = mapper;
}
public override void Configure()
{
Get("/workflow-definitions/by-definition-id/{definitionId}", "/workflow-definitions/{definitionId}");
@ -43,16 +32,17 @@ internal class GetByDefinitionId : ElsaEndpoint<Request>
};
var order = new WorkflowDefinitionOrder<int>(x => x.Version, OrderDirection.Descending);
var definition = (await _store.FindManyAsync(filter, order, cancellationToken: cancellationToken)).FirstOrDefault();
var definition = (await store.FindManyAsync(filter, order, cancellationToken: cancellationToken)).FirstOrDefault();
if (definition == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var model = await _mapper.MapAsync(definition, cancellationToken);
var serializerOptions = _apiSerializer.GetOptions();
var model = await linker.MapAsync(definition, cancellationToken);
var serializerOptions = apiSerializer.GetOptions();
// If the root of composite activities is not requested, exclude them from being serialized.
if (!request.IncludeCompositeRoot)

View file

@ -3,7 +3,6 @@ using Elsa.Extensions;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Serialization.Converters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
@ -11,19 +10,9 @@ using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetById;
[PublicAPI]
internal class GetById : ElsaEndpoint<Request>
internal class GetById(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, IWorkflowDefinitionLinker linker)
: ElsaEndpoint<Request>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IApiSerializer _apiSerializer;
private readonly WorkflowDefinitionMapper _mapper;
public GetById(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, WorkflowDefinitionMapper mapper)
{
_store = store;
_apiSerializer = apiSerializer;
_mapper = mapper;
}
public override void Configure()
{
Get("/workflow-definitions/by-id/{id}");
@ -37,7 +26,7 @@ internal class GetById : ElsaEndpoint<Request>
Id = request.Id
};
var definition = await _store.FindAsync(filter, cancellationToken);
var definition = await store.FindAsync(filter, cancellationToken);
if (definition == null)
{
@ -45,8 +34,8 @@ internal class GetById : ElsaEndpoint<Request>
return;
}
var model = await _mapper.MapAsync(definition, cancellationToken);
var serializerOptions = _apiSerializer.GetOptions().Clone();
var model = await linker.MapAsync(definition, cancellationToken);
var serializerOptions = apiSerializer.GetOptions().Clone();
// If the root of composite activities is not requested, exclude them from being serialized.
if (!request.IncludeCompositeRoot)

View file

@ -1,11 +1,10 @@
using Elsa.Abstractions;
using Elsa.Extensions;
using Elsa.Models;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Serialization.Converters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
@ -13,19 +12,9 @@ using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetManyById;
[PublicAPI]
internal class GetManyById : ElsaEndpoint<Request>
internal class GetManyById(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, IWorkflowDefinitionLinker linker)
: ElsaEndpoint<Request>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IApiSerializer _apiSerializer;
private readonly WorkflowDefinitionMapper _mapper;
public GetManyById(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, WorkflowDefinitionMapper mapper)
{
_store = store;
_apiSerializer = apiSerializer;
_mapper = mapper;
}
public override void Configure()
{
Get("/workflow-definitions/many-by-id");
@ -39,15 +28,16 @@ internal class GetManyById : ElsaEndpoint<Request>
Ids = request.Ids
};
var definitions = (await _store.FindManyAsync(filter, cancellationToken)).ToList();
var models = (await _mapper.MapAsync(definitions, cancellationToken)).ToList();
var serializerOptions = _apiSerializer.GetOptions().Clone();
var definitions = (await store.FindManyAsync(filter, cancellationToken)).ToList();
var serializerOptions = apiSerializer.GetOptions().Clone();
// If the root of composite activities is not requested, exclude them from being serialized.
if (!request.IncludeCompositeRoot)
serializerOptions.Converters.Add(new JsonIgnoreCompositeRootConverterFactory());
var response = new ListResponse<WorkflowDefinitionModel>(models);
var models = await linker.MapAsync(definitions, cancellationToken);
var response = new ListResponse<LinkedWorkflowDefinitionModel>(models);
await HttpContext.Response.WriteAsJsonAsync(response, serializerOptions, cancellationToken);
}
}

View file

@ -1,9 +1,10 @@
using Elsa.Abstractions;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Import;
@ -13,22 +14,19 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Import;
[PublicAPI]
internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
{
private readonly IWorkflowDefinitionService _workflowDefinitionService;
private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
private readonly IApiSerializer _apiSerializer;
private readonly IWorkflowDefinitionLinker _linker;
private readonly IAuthorizationService _authorizationService;
/// <inheritdoc />
public Import(
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowDefinitionImporter workflowDefinitionImporter,
WorkflowDefinitionMapper workflowDefinitionMapper,
IApiSerializer apiSerializer)
IWorkflowDefinitionLinker linker,
IAuthorizationService authorizationService)
{
_workflowDefinitionService = workflowDefinitionService;
_workflowDefinitionImporter = workflowDefinitionImporter;
_workflowDefinitionMapper = workflowDefinitionMapper;
_apiSerializer = apiSerializer;
_linker = linker;
_authorizationService = authorizationService;
}
/// <inheritdoc />
@ -46,7 +44,16 @@ internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
var isNew = string.IsNullOrWhiteSpace(definitionId);
var result = await ImportSingleWorkflowDefinitionAsync(model, cancellationToken);
var definition = result.WorkflowDefinition;
var updatedModel = await _workflowDefinitionMapper.MapAsync(definition, cancellationToken);
var authorizationResult = _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var updatedModel = await _linker.MapAsync(definition, cancellationToken);
if (result.Succeeded)
{

View file

@ -1,10 +1,13 @@
using System.IO.Compression;
using Elsa.Abstractions;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.ImportFiles;
@ -19,18 +22,21 @@ internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
private readonly IApiSerializer _apiSerializer;
private readonly IAuthorizationService _authorizationService;
/// <inheritdoc />
public ImportFiles(
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowDefinitionImporter workflowDefinitionImporter,
WorkflowDefinitionMapper workflowDefinitionMapper,
IApiSerializer apiSerializer)
IApiSerializer apiSerializer,
IAuthorizationService authorizationService)
{
_workflowDefinitionService = workflowDefinitionService;
_workflowDefinitionImporter = workflowDefinitionImporter;
_workflowDefinitionMapper = workflowDefinitionMapper;
_apiSerializer = apiSerializer;
_authorizationService = authorizationService;
}
/// <inheritdoc />
@ -44,14 +50,22 @@ internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
/// <inheritdoc />
public override async Task HandleAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken)
{
var authorizationResult = _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
if (Files.Any())
{
var count = await ImportFilesAsync(Files, cancellationToken);
if (!ValidationFailed)
await SendOkAsync(new { Count = count }, cancellationToken);
}
if (ValidationFailed)
await SendErrorsAsync(400, cancellationToken);
}
@ -59,7 +73,7 @@ internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
private async Task<int> ImportFilesAsync(IFormFileCollection files, CancellationToken cancellationToken)
{
var count = 0;
foreach (var file in files)
{
var fileStream = file.OpenReadStream();
@ -89,7 +103,7 @@ internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
}
}
}
return count;
}

View file

@ -8,15 +8,8 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.IsNameUnique;
/// Checks if a workflow definition name is unique.
/// </summary>
[PublicAPI]
internal class IsNameUnique : ElsaEndpoint<Request>
internal class IsNameUnique(IWorkflowDefinitionStore store) : ElsaEndpoint<Request>
{
private readonly IWorkflowDefinitionStore _store;
public IsNameUnique(IWorkflowDefinitionStore store)
{
_store = store;
}
public override void Configure()
{
Get("/workflow-definitions/validation/is-name-unique");
@ -25,7 +18,7 @@ internal class IsNameUnique : ElsaEndpoint<Request>
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var isUnique = await _store.GetIsNameUnique(request.Name.Trim(), request.DefinitionId, cancellationToken);
var isUnique = await store.GetIsNameUnique(request.Name.Trim(), request.DefinitionId, cancellationToken);
var response = new Response(isUnique);
await SendOkAsync(response, cancellationToken);

View file

@ -11,7 +11,7 @@ using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.List;
[PublicAPI]
internal class List(IWorkflowDefinitionStore store) : ElsaEndpoint<Request, PagedListResponse<WorkflowDefinitionSummary>>
internal class List(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker linker) : ElsaEndpoint<Request, PagedListResponse<LinkedWorkflowDefinitionSummary>>
{
public override void Configure()
{
@ -19,12 +19,14 @@ internal class List(IWorkflowDefinitionStore store) : ElsaEndpoint<Request, Page
ConfigurePermissions("read:workflow-definitions");
}
public override async Task<PagedListResponse<WorkflowDefinitionSummary>> ExecuteAsync(Request request, CancellationToken cancellationToken)
public override async Task<PagedListResponse<LinkedWorkflowDefinitionSummary>> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var pageArgs = PageArgs.FromPage(request.Page, request.PageSize);
var filter = CreateFilter(request);
var summaries = await FindAsync(request, filter, pageArgs, cancellationToken);
return new PagedListResponse<WorkflowDefinitionSummary>(summaries);
var pagedList = new PagedListResponse<WorkflowDefinitionSummary>(summaries);
var response = linker.MapAsync(pagedList);
return response;
}
private WorkflowDefinitionFilter CreateFilter(Request request)

View file

@ -1,45 +1,35 @@
using System.Text.Json;
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Medallion.Threading;
using System.Text.Json;
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Models;
using Elsa.Workflows.Serialization.Converters;
using JetBrains.Annotations;
using Medallion.Threading;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Elsa.Extensions;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Post;
[PublicAPI]
internal class Post : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefinitionModel>
internal class Post(
IApiSerializer serializer,
IWorkflowDefinitionPublisher workflowDefinitionPublisher,
VariableDefinitionMapper variableDefinitionMapper,
IDistributedLockProvider distributedLockProvider,
IWorkflowDefinitionLinker linker,
IAuthorizationService authorizationService)
: ElsaEndpoint<SaveWorkflowDefinitionRequest, LinkedWorkflowDefinitionModel>
{
private readonly IApiSerializer _serializer;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
private readonly VariableDefinitionMapper _variableDefinitionMapper;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
private readonly IDistributedLockProvider _distributedLockProvider;
public Post(
IApiSerializer serializer,
IWorkflowDefinitionPublisher workflowDefinitionPublisher,
VariableDefinitionMapper variableDefinitionMapper,
WorkflowDefinitionMapper workflowDefinitionMapper,
IDistributedLockProvider distributedLockProvider)
{
_serializer = serializer;
_workflowDefinitionPublisher = workflowDefinitionPublisher;
_variableDefinitionMapper = variableDefinitionMapper;
_workflowDefinitionMapper = workflowDefinitionMapper;
_distributedLockProvider = distributedLockProvider;
}
public override void Configure()
{
Post("/workflow-definitions");
@ -52,10 +42,10 @@ internal class Post : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefini
var definitionId = model.DefinitionId;
var resourceName = $"{GetType().FullName}:{(!string.IsNullOrWhiteSpace(definitionId) ? definitionId : Guid.NewGuid().ToString())}";
await using var handle = await _distributedLockProvider.AcquireLockAsync(resourceName, TimeSpan.FromMinutes(1), cancellationToken);
await using var handle = await distributedLockProvider.AcquireLockAsync(resourceName, TimeSpan.FromMinutes(1), cancellationToken);
var draft = !string.IsNullOrWhiteSpace(definitionId)
? await _workflowDefinitionPublisher.GetDraftAsync(definitionId, VersionOptions.Latest, cancellationToken)
? await workflowDefinitionPublisher.GetDraftAsync(definitionId, VersionOptions.Latest, cancellationToken)
: default;
var isNew = draft == null;
@ -63,21 +53,29 @@ internal class Post : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefini
// Create a new workflow in case no existing definition was found.
if (isNew)
{
draft = _workflowDefinitionPublisher.New();
draft = workflowDefinitionPublisher.New();
if (!string.IsNullOrWhiteSpace(definitionId))
draft.DefinitionId = definitionId;
}
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(draft), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
// Update the draft with the received model.
var root = model.Root ?? new Sequence();
var serializerOptions = _serializer.GetOptions().Clone();
var serializerOptions = serializer.GetOptions().Clone();
// Ignore the root activity when serializing the workflow definition.
serializerOptions.Converters.Add(new JsonIgnoreCompositeRootConverterFactory());
var stringData = JsonSerializer.Serialize(root, serializerOptions);
var variables = _variableDefinitionMapper.Map(model.Variables).ToList();
var variables = variableDefinitionMapper.Map(model.Variables).ToList();
var inputs = model.Inputs ?? new List<InputDefinition>();
var outputs = model.Outputs ?? new List<OutputDefinition>();
var outcomes = model.Outcomes ?? new List<string>();
@ -96,11 +94,11 @@ internal class Post : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefini
if (request.Publish.GetValueOrDefault(false))
{
var result = await _workflowDefinitionPublisher.PublishAsync(draft, cancellationToken);
var result = await workflowDefinitionPublisher.PublishAsync(draft, cancellationToken);
if (!result.Succeeded)
{
foreach (var validationError in result.ValidationErrors)
foreach (var validationError in result.ValidationErrors)
AddError(validationError.Message);
await SendErrorsAsync(400, cancellationToken);
@ -109,10 +107,10 @@ internal class Post : ElsaEndpoint<SaveWorkflowDefinitionRequest, WorkflowDefini
}
else
{
await _workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken);
await workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken);
}
var response = await _workflowDefinitionMapper.MapAsync(draft, cancellationToken);
var response = await linker.MapAsync(draft, cancellationToken);
if (isNew)
await SendCreatedAtAsync<GetByDefinitionId.GetByDefinitionId>(new { definitionId }, response, cancellation: cancellationToken);

View file

@ -1,33 +1,23 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Extensions;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Serialization.Converters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Publish;
[PublicAPI]
internal class Publish : ElsaEndpoint<Request, WorkflowDefinitionModel>
internal class Publish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IApiSerializer serializer, IWorkflowDefinitionLinker linker, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, LinkedWorkflowDefinitionModel>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
private readonly IApiSerializer _serializer;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
public Publish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IApiSerializer serializer, WorkflowDefinitionMapper workflowDefinitionMapper)
{
_store = store;
_workflowDefinitionPublisher = workflowDefinitionPublisher;
_serializer = serializer;
_workflowDefinitionMapper = workflowDefinitionMapper;
}
public override void Configure()
{
Post("/workflow-definitions/{definitionId}/publish");
@ -42,7 +32,7 @@ internal class Publish : ElsaEndpoint<Request, WorkflowDefinitionModel>
VersionOptions = VersionOptions.Latest
};
var definition = await _store.FindAsync(filter, cancellationToken);
var definition = await store.FindAsync(filter, cancellationToken);
if (definition == null)
{
@ -50,6 +40,14 @@ internal class Publish : ElsaEndpoint<Request, WorkflowDefinitionModel>
return;
}
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
if (definition.IsPublished)
{
AddError($"Workflow with id {request.DefinitionId} is already published");
@ -57,12 +55,12 @@ internal class Publish : ElsaEndpoint<Request, WorkflowDefinitionModel>
return;
}
await _workflowDefinitionPublisher.PublishAsync(definition, cancellationToken);
await workflowDefinitionPublisher.PublishAsync(definition, cancellationToken);
var response = await _workflowDefinitionMapper.MapAsync(definition, cancellationToken);
var response = await linker.MapAsync(definition, cancellationToken);
// We do not want to include composite root activities in the response.
var serializerOptions = _serializer.GetOptions().Clone();
var serializerOptions = serializer.GetOptions().Clone();
serializerOptions.Converters.Add(new JsonIgnoreCompositeRootConverterFactory());
await HttpContext.Response.WriteAsJsonAsync(response, serializerOptions, cancellationToken);

View file

@ -1,27 +1,19 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Retract;
[PublicAPI]
internal class Retract : ElsaEndpoint<Request, WorkflowDefinitionModel>
internal class Retract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IWorkflowDefinitionLinker linker, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, LinkedWorkflowDefinitionModel>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IWorkflowDefinitionPublisher _workflowDefinitionPublisher;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
public Retract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, WorkflowDefinitionMapper workflowDefinitionMapper)
{
_store = store;
_workflowDefinitionPublisher = workflowDefinitionPublisher;
_workflowDefinitionMapper = workflowDefinitionMapper;
}
public override void Configure()
{
Post("/workflow-definitions/{definitionId}/retract");
@ -35,8 +27,8 @@ internal class Retract : ElsaEndpoint<Request, WorkflowDefinitionModel>
DefinitionId = request.DefinitionId,
VersionOptions = VersionOptions.LatestOrPublished
};
var definition = await _store.FindAsync(filter, cancellationToken);
var definition = await store.FindAsync(filter, cancellationToken);
if (definition == null)
{
@ -44,6 +36,14 @@ internal class Retract : ElsaEndpoint<Request, WorkflowDefinitionModel>
return;
}
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
if (!definition.IsPublished)
{
AddError($"Workflow with id {request.DefinitionId} is not published");
@ -51,8 +51,8 @@ internal class Retract : ElsaEndpoint<Request, WorkflowDefinitionModel>
return;
}
await _workflowDefinitionPublisher.RetractAsync(definition, cancellationToken);
var response = await _workflowDefinitionMapper.MapAsync(definition, cancellationToken);
await workflowDefinitionPublisher.RetractAsync(definition, cancellationToken);
var response = await linker.MapAsync(definition, cancellationToken);
await SendOkAsync(response, cancellationToken);
}
}

View file

@ -1,23 +1,18 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.UpdateReferences;
[PublicAPI]
internal class UpdateReferences : ElsaEndpoint<Request, Response>
internal class UpdateReferences(IWorkflowDefinitionStore store, IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
public UpdateReferences(IWorkflowDefinitionStore store, IWorkflowDefinitionManager workflowDefinitionManager)
{
_store = store;
_workflowDefinitionManager = workflowDefinitionManager;
}
public override void Configure()
{
Post("/workflow-definitions/{definitionId}/update-references");
@ -32,7 +27,7 @@ internal class UpdateReferences : ElsaEndpoint<Request, Response>
VersionOptions = VersionOptions.Latest
};
var definition = await _store.FindAsync(filter, cancellationToken);
var definition = await store.FindAsync(filter, cancellationToken);
if (definition == null)
{
@ -40,7 +35,15 @@ internal class UpdateReferences : ElsaEndpoint<Request, Response>
return;
}
var affectedWorkflows = await _workflowDefinitionManager.UpdateReferencesInConsumingWorkflows(definition, cancellationToken);
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var affectedWorkflows = await workflowDefinitionManager.UpdateReferencesInConsumingWorkflows(definition, cancellationToken);
var response = new Response(affectedWorkflows.Select(w => w.Name ?? w.DefinitionId));
await SendOkAsync(response, cancellationToken);
}

View file

@ -1,7 +1,11 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management.Contracts;
using FastEndpoints;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
@ -12,11 +16,15 @@ namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
public class DeleteVersion : ElsaEndpointWithoutRequest
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
private readonly IAuthorizationService _authorizationService;
private readonly IWorkflowDefinitionStore _store;
/// <inheritdoc />
public DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager)
public DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService, IWorkflowDefinitionStore store)
{
_workflowDefinitionManager = workflowDefinitionManager;
_authorizationService = authorizationService;
_store = store;
}
/// <inheritdoc />
@ -27,19 +35,41 @@ public class DeleteVersion : ElsaEndpointWithoutRequest
}
/// <inheritdoc />
public override async Task HandleAsync(CancellationToken ct)
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var definitionId = Route<string>("definitionId")!;
var version = Route<int>("version");
var result = await _workflowDefinitionManager.DeleteVersionAsync(definitionId, version, ct);
if (!result)
var filter = new WorkflowDefinitionFilter
{
await SendNotFoundAsync(ct);
DefinitionId = definitionId,
VersionOptions = VersionOptions.SpecificVersion(version)
};
var definition = await _store.FindAsync(filter, cancellationToken);
if (definition == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(ct);
var authorizationResult = _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var result = await _workflowDefinitionManager.DeleteVersionAsync(definition, cancellationToken);
if (!result)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(cancellationToken);
}
}

View file

@ -3,22 +3,14 @@ using Elsa.Common.Entities;
using Elsa.Common.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using FastEndpoints;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
[PublicAPI]
internal class ListVersions : ElsaEndpointWithoutRequest
internal class ListVersions(IWorkflowDefinitionStore store) : ElsaEndpointWithoutRequest
{
private readonly IWorkflowDefinitionStore _store;
public ListVersions(IWorkflowDefinitionStore store)
{
_store = store;
}
public override void Configure()
{
Get("workflow-definitions/{definitionId}/versions");
@ -36,7 +28,7 @@ internal class ListVersions : ElsaEndpointWithoutRequest
};
var orderBy = new WorkflowDefinitionOrder<int>(x => x.Version, OrderDirection.Descending);
var definitions = await _store.FindManyAsync(filter, orderBy, cancellationToken);
var definitions = await store.FindManyAsync(filter, orderBy, cancellationToken);
if (!definitions.Any())
{

View file

@ -1,33 +1,53 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management.Contracts;
using FastEndpoints;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
[PublicAPI]
internal class RevertVersion : ElsaEndpointWithoutRequest
internal class RevertVersion(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService, IWorkflowDefinitionStore store)
: ElsaEndpointWithoutRequest
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
public RevertVersion(IWorkflowDefinitionManager workflowDefinitionManager)
{
_workflowDefinitionManager = workflowDefinitionManager;
}
public override void Configure()
{
Post("workflow-definitions/{definitionId}/revert/{version}");
ConfigurePermissions("publish:workflow-definitions");
}
public override async Task HandleAsync(CancellationToken ct)
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var definitionId = Route<string>("definitionId")!;
var version = Route<int>("version");
await _workflowDefinitionManager.RevertVersionAsync(definitionId, version, ct);
await SendOkAsync(ct);
var filter = new WorkflowDefinitionFilter
{
DefinitionId = definitionId,
VersionOptions = VersionOptions.SpecificVersion(version)
};
var definition = await store.FindAsync(filter, cancellationToken);
if (definition == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var authorizationResult = authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Result.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
await workflowDefinitionManager.RevertVersionAsync(definitionId, version, cancellationToken);
await SendOkAsync(cancellationToken);
}
}

View file

@ -28,12 +28,7 @@ public class Request
public VersionOptions? VersionOptions { get; set; }
}
public class Response
public class Response(int cancelledCount)
{
public Response(int cancelledCount)
{
CancelledCount = cancelledCount;
}
[JsonPropertyName("cancelled")] public int CancelledCount { get; }
[JsonPropertyName("cancelled")] public int CancelledCount { get; } = cancelledCount;
}

View file

@ -6,15 +6,8 @@ using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.BulkDelete;
[PublicAPI]
internal class BulkDelete : ElsaEndpoint<Request, Response>
internal class BulkDelete(IWorkflowInstanceManager store) : ElsaEndpoint<Request, Response>
{
private readonly IWorkflowInstanceManager _store;
public BulkDelete(IWorkflowInstanceManager store)
{
_store = store;
}
public override void Configure()
{
Post("/bulk-actions/delete/workflow-instances/by-id");
@ -24,7 +17,7 @@ internal class BulkDelete : ElsaEndpoint<Request, Response>
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var filter = new WorkflowInstanceFilter { Ids = request.Ids };
var count = await _store.BulkDeleteAsync(filter, cancellationToken);
var count = await store.BulkDeleteAsync(filter, cancellationToken);
return new Response(count);
}

View file

@ -7,12 +7,7 @@ internal class Request
public ICollection<string> Ids { get; set; } = default!;
}
internal class Response
internal class Response(long deletedCount)
{
public Response(long deletedCount)
{
DeletedCount = deletedCount;
}
[JsonPropertyName("deleted")] public long DeletedCount { get; }
[JsonPropertyName("deleted")] public long DeletedCount { get; } = deletedCount;
}

View file

@ -6,11 +6,8 @@ using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Delete;
[PublicAPI]
internal class Delete : ElsaEndpoint<Request>
internal class Delete(IWorkflowInstanceManager store) : ElsaEndpoint<Request>
{
private readonly IWorkflowInstanceManager _store;
public Delete(IWorkflowInstanceManager store) => _store = store;
public override void Configure()
{
Delete("/workflow-instances/{id}");
@ -20,7 +17,7 @@ internal class Delete : ElsaEndpoint<Request>
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var filter = new WorkflowInstanceFilter { Id = request.Id };
var deleted = await _store.DeleteAsync(filter, cancellationToken);
var deleted = await store.DeleteAsync(filter, cancellationToken);
if (deleted)
await SendNoContentAsync(cancellationToken);

View file

@ -7,15 +7,8 @@ using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Get;
[PublicAPI]
internal class Get : ElsaEndpoint<Request, WorkflowInstanceModel, WorkflowInstanceMapper>
internal class Get(IWorkflowInstanceStore store) : ElsaEndpoint<Request, WorkflowInstanceModel, WorkflowInstanceMapper>
{
private readonly IWorkflowInstanceStore _store;
public Get(IWorkflowInstanceStore store)
{
_store = store;
}
public override void Configure()
{
Get("/workflow-instances/{id}");
@ -25,7 +18,7 @@ internal class Get : ElsaEndpoint<Request, WorkflowInstanceModel, WorkflowInstan
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var filter = new WorkflowInstanceFilter { Id = request.Id };
var workflowInstance = await _store.FindAsync(filter, cancellationToken);
var workflowInstance = await store.FindAsync(filter, cancellationToken);
if (workflowInstance == null)
await SendNotFoundAsync(cancellationToken);

View file

@ -47,14 +47,8 @@ internal class JournalFilter
public ICollection<string>? EventNames { get; set; }
}
internal class Response
internal class Response(ICollection<ExecutionLogRecord> items, long totalCount)
{
public Response(ICollection<ExecutionLogRecord> items, long totalCount)
{
Items = items;
TotalCount = totalCount;
}
public ICollection<ExecutionLogRecord> Items { get; }
public long TotalCount { get; }
public ICollection<ExecutionLogRecord> Items { get; } = items;
public long TotalCount { get; } = totalCount;
}

View file

@ -36,14 +36,8 @@ internal class Request
public int? Take { get; set; }
}
internal class Response
internal class Response(ICollection<ExecutionLogRecord> items, long totalCount)
{
public Response(ICollection<ExecutionLogRecord> items, long totalCount)
{
Items = items;
TotalCount = totalCount;
}
public ICollection<ExecutionLogRecord> Items { get; }
public long TotalCount { get; }
public ICollection<ExecutionLogRecord> Items { get; } = items;
public long TotalCount { get; } = totalCount;
}

View file

@ -4,9 +4,14 @@ using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Http.Features;
using Elsa.SasTokens.Features;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Api.Serialization;
using Elsa.Workflows.Api.Services;
using Elsa.Workflows.Management.Features;
using Elsa.Workflows.Runtime.Features;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Api.Features;
@ -36,5 +41,13 @@ public class WorkflowsApiFeature : FeatureBase
{
Services.AddSerializationOptionsConfigurator<SerializationConfigurator>();
Module.AddFastEndpointsFromModule();
Services.AddScoped<IWorkflowDefinitionLinker, StaticWorkflowDefinitionLinker>();
Services.AddScoped<IAuthorizationHandler, NotReadOnlyRequirementHandler>();
Services.Configure<AuthorizationOptions>(options =>
{
options.AddPolicy(AuthorizationPolicies.NotReadOnlyPolicy, policy => policy.AddRequirements(new NotReadOnlyRequirement()));
});
}
}

View file

@ -6,11 +6,8 @@ namespace Elsa.Workflows.Api.Middleware;
/// <summary>
/// Catches JSON serialization errors during POST requests and returns Bad Request responses.
/// </summary>
public class JsonSerializationErrorHandlerMiddleware
public class JsonSerializationErrorHandlerMiddleware(RequestDelegate next)
{
private readonly RequestDelegate _next;
public JsonSerializationErrorHandlerMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext httpContext)
{
var method = httpContext.Request.Method;
@ -18,13 +15,13 @@ public class JsonSerializationErrorHandlerMiddleware
// If the HTTP verb is anything but POST or PUT, do nothing.
if (!string.Equals(method, "POST", StringComparison.InvariantCultureIgnoreCase) && !string.Equals(method, "PUT", StringComparison.InvariantCultureIgnoreCase))
{
await _next(httpContext);
await next(httpContext);
return;
}
try
{
await _next(httpContext);
await next(httpContext);
}
catch (JsonException e)
{

View file

@ -0,0 +1,6 @@
using Elsa.Models;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Api.Models;
public record LinkedWorkflowDefinitionModel(Link[]? Links) : WorkflowDefinitionModel;

View file

@ -0,0 +1,9 @@
using Elsa.Models;
using Elsa.Workflows.Management.Models;
namespace Elsa.Workflows.Api.Models;
public class LinkedWorkflowDefinitionSummary : WorkflowDefinitionSummary
{
public Link[]? Links { get; set; }
}

View file

@ -0,0 +1,43 @@
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Options;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
namespace Elsa.Workflows.Api.Requirements;
public record NotReadOnlyResource(WorkflowDefinition? WorkflowDefinition = default);
public record NotReadOnlyRequirement() : IAuthorizationRequirement;
/// <inheritdoc />
[PublicAPI]
public class NotReadOnlyRequirementHandler : AuthorizationHandler<NotReadOnlyRequirement, NotReadOnlyResource>
{
private readonly IOptions<ManagementOptions> _managementOptions;
/// <inheritdoc />
public NotReadOnlyRequirementHandler(
IOptions<ManagementOptions> managementOptions)
{
_managementOptions = managementOptions;
}
/// <inheritdoc />
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, NotReadOnlyRequirement requirement, NotReadOnlyResource resource)
{
if (_managementOptions.Value.IsReadOnlyMode)
{
context.Fail(new AuthorizationFailureReason(this, "Workflow edit is not allowed when the read-only mode is enabled."));
}
if (resource.WorkflowDefinition != null && (resource.WorkflowDefinition.IsReadonly || resource.WorkflowDefinition.IsSystem))
{
context.Fail(new AuthorizationFailureReason(this, "Workflow edit is not allowed for a readonly or system workflow."));
}
context.Succeed(requirement);
}
}

View file

@ -5,19 +5,12 @@ using Elsa.Workflows.Models;
namespace Elsa.Workflows.Api.Serialization;
internal class ArgumentJsonConverterFactory : JsonConverterFactory
internal class ArgumentJsonConverterFactory(IWellKnownTypeRegistry wellKnownTypeRegistry) : JsonConverterFactory
{
private readonly IWellKnownTypeRegistry _wellKnownTypeRegistry;
public ArgumentJsonConverterFactory(IWellKnownTypeRegistry wellKnownTypeRegistry)
{
_wellKnownTypeRegistry = wellKnownTypeRegistry;
}
public override bool CanConvert(Type typeToConvert) => typeToConvert.IsAssignableTo(typeof(ArgumentDefinition));
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return new ArgumentJsonConverter(_wellKnownTypeRegistry);
return new ArgumentJsonConverter(wellKnownTypeRegistry);
}
}

View file

@ -3,17 +3,10 @@ using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Api.Serialization;
internal class SerializationConfigurator : SerializationOptionsConfiguratorBase
internal class SerializationConfigurator(IServiceProvider serviceProvider) : SerializationOptionsConfiguratorBase
{
private readonly IServiceProvider _serviceProvider;
public SerializationConfigurator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public override void Configure(JsonSerializerOptions options)
{
options.Converters.Add(ActivatorUtilities.GetServiceOrCreateInstance<ArgumentJsonConverterFactory>(_serviceProvider));
options.Converters.Add(ActivatorUtilities.GetServiceOrCreateInstance<ArgumentJsonConverterFactory>(serviceProvider));
}
}

View file

@ -0,0 +1,165 @@
using Elsa.Models;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Management.Options;
using Microsoft.Extensions.Options;
namespace Elsa.Workflows.Api.Services;
/// <inheritdoc/>
public class StaticWorkflowDefinitionLinker(
IOptions<ManagementOptions> managementOptions,
WorkflowDefinitionMapper workflowDefinitionMapper)
: IWorkflowDefinitionLinker
{
/// <inheritdoc />
public async Task<LinkedWorkflowDefinitionModel> MapAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default)
{
var workflowDefinitionModel = await workflowDefinitionMapper.MapAsync(definition, cancellationToken);
var linkedModel = new LinkedWorkflowDefinitionModel(GenerateLinksForSingleEntry(definition.DefinitionId, definition.IsReadonly))
{
Id = workflowDefinitionModel.Id,
DefinitionId = workflowDefinitionModel.DefinitionId,
Name = workflowDefinitionModel.Name,
Description = workflowDefinitionModel.Description,
CreatedAt = workflowDefinitionModel.CreatedAt,
Version = workflowDefinitionModel.Version,
ToolVersion = workflowDefinitionModel.ToolVersion,
Variables = workflowDefinitionModel.Variables,
Inputs = workflowDefinitionModel.Inputs,
Outputs = workflowDefinitionModel.Outputs,
Outcomes = workflowDefinitionModel.Outcomes,
CustomProperties = workflowDefinitionModel.CustomProperties,
IsReadonly = workflowDefinitionModel.IsReadonly,
IsSystem = workflowDefinitionModel.IsSystem,
IsLatest = workflowDefinitionModel.IsLatest,
IsPublished = workflowDefinitionModel.IsPublished,
Options = workflowDefinitionModel.Options,
UsableAsActivity = workflowDefinitionModel.UsableAsActivity,
Root = workflowDefinitionModel.Root
};
return linkedModel;
}
/// <inheritdoc />
public PagedListResponse<LinkedWorkflowDefinitionSummary> MapAsync(PagedListResponse<WorkflowDefinitionSummary> list, CancellationToken cancellationToken = default)
{
var items = new List<LinkedWorkflowDefinitionSummary>();
foreach (var item in list.Items)
{
items.Add(new LinkedWorkflowDefinitionSummary
{
Links = GenerateLinksForSingleEntry(item.DefinitionId, item.IsReadonly),
Id = item.Id,
DefinitionId = item.DefinitionId,
Name = item.Name,
Description = item.Description,
Version = item.Version,
ToolVersion = item.ToolVersion,
IsLatest = item.IsLatest,
IsPublished = item.IsPublished,
ProviderName = item.ProviderName,
MaterializerName = item.MaterializerName,
CreatedAt = item.CreatedAt,
IsReadonly = item.IsReadonly
});
}
return new PagedListResponse<LinkedWorkflowDefinitionSummary>
{
TotalCount = list.TotalCount,
Items = items,
Links = GenerateLinksForPagedList().ToArray()
};
}
/// <inheritdoc />
public async Task<List<LinkedWorkflowDefinitionModel>> MapAsync(List<WorkflowDefinition> definitions, CancellationToken cancellationToken = default)
{
var models = (await workflowDefinitionMapper.MapAsync(definitions, cancellationToken)).ToList();
var result = new List<LinkedWorkflowDefinitionModel>();
foreach (var item in models)
{
var linkedModel = new LinkedWorkflowDefinitionModel(GenerateLinksForSingleEntry(item.DefinitionId, item.IsReadonly))
{
Id = item.Id,
DefinitionId = item.DefinitionId,
Name = item.Name,
Description = item.Description,
CreatedAt = item.CreatedAt,
Version = item.Version,
ToolVersion = item.ToolVersion,
Variables = item.Variables,
Inputs = item.Inputs,
Outputs = item.Outputs,
Outcomes = item.Outcomes,
CustomProperties = item.CustomProperties,
IsReadonly = item.IsReadonly,
IsSystem = item.IsSystem,
IsLatest = item.IsLatest,
IsPublished = item.IsPublished,
Options = item.Options,
UsableAsActivity = item.UsableAsActivity,
Root = item.Root
};
result.Add(linkedModel);
}
return result;
}
private Link[] GenerateLinksForPagedList()
{
var linksList = new List<Link>
{
new($"/workflow-definitions", "self", "GET"),
new($"/workflow-definitions/validation/is-name-unique", "is-name-unique", "GET"),
new($"/workflow-definitions/query/count", "count", "GET"),
new($"/workflow-definitions/many-by-id", "many-by-id", "GET")
};
if (!managementOptions.Value.IsReadOnlyMode)
{
linksList.Add(new Link($"/bulk-actions/delete/workflow-definitions/by-definition-id", "bulk-delete-by-definition-id", "POST"));
linksList.Add(new Link($"/bulk-actions/delete/workflow-definitions/by-id", "bulk-delete-by-id", "POST"));
linksList.Add(new Link($"/bulk-actions/publish/workflow-definitions/by-definition-ids", "bulk-publish", "POST"));
linksList.Add(new Link($"/bulk-actions/retract/workflow-definitions/by-definition-ids", "bulk-retract", "POST"));
linksList.Add(new Link($"/workflow-definitions/import", "import", "POST"));
linksList.Add(new Link($"/workflow-definitions/import-files", "import-files", "POST"));
linksList.Add(new Link($"/workflow-definitions", "create", "POST"));
}
return linksList.ToArray();
}
private Link[] GenerateLinksForSingleEntry(string definitionId, bool definitionIsReadonly)
{
var links = new List<Link>
{
new($"/workflow-definitions/{definitionId}", "self", "GET"),
new($"/workflow-definitions/by-definition-id/{definitionId}", "self", "GET"),
new($"/workflow-definitions/{definitionId}/versions", "versions", "GET"),
new($"/workflow-definitions/{definitionId}/bulk-dispatch", "bulk-dispatch", "POST"),
new($"/workflow-definitions/{definitionId}/dispatch", "dispatch", "POST"),
new($"/workflow-definitions/{definitionId}/execute", "execute", "POST"),
new($"/workflow-definitions/{definitionId}/export", "export", "POST")
};
if (!managementOptions.Value.IsReadOnlyMode && !definitionIsReadonly)
{
links.Add(new Link($"/workflow-definitions/{definitionId}/publish", "publish", "POST"));
links.Add(new Link($"/workflow-definitions/{definitionId}/retract", "retract", "POST"));
links.Add(new Link($"/workflow-definitions/{definitionId}", "delete", "DELETE"));
links.Add(new Link($"/workflow-definitions/{definitionId}/import", "import", "PUT"));
links.Add(new Link($"/workflow-definitions/{definitionId}/update-references", "update-references", "POST"));
}
return links.ToArray();
}
}

View file

@ -31,7 +31,15 @@ public interface IWorkflowDefinitionManager
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>True if the workflow definition version was deleted, otherwise false.</returns>
Task<bool> DeleteVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a specific workflow definition version.
/// </summary>
/// <param name="definitionToDelete"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<bool> DeleteVersionAsync(WorkflowDefinition definitionToDelete, CancellationToken cancellationToken);
/// <summary>
/// Deletes all workflow definition versions with the specified definition IDs.
/// </summary>

View file

@ -19,6 +19,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj" />
<ProjectReference Include="..\Elsa.Caching\Elsa.Caching.csproj" />
<ProjectReference Include="..\Elsa.Dsl\Elsa.Dsl.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />

View file

@ -47,6 +47,8 @@ public class WorkflowManagementFeature : FeatureBase
private string CompressionAlgorithm { get; set; } = nameof(None);
private LogPersistenceMode LogPersistenceMode { get; set; } = LogPersistenceMode.Include;
private bool IsReadOnlyMode { get; set; }
/// <inheritdoc />
public WorkflowManagementFeature(IModule module) : base(module)
{
@ -175,6 +177,16 @@ public class WorkflowManagementFeature : FeatureBase
return this;
}
/// <summary>
/// Enables or disables read-only mode for resources such as workflow definitions.
/// </summary>
/// <returns></returns>
public WorkflowManagementFeature UseReadOnlyMode(bool enabled)
{
IsReadOnlyMode = enabled;
return this;
}
/// <inheritdoc />
[RequiresUnreferencedCode("The assembly containing the specified marker type will be scanned for activity types.")]
public override void Configure()
@ -229,6 +241,7 @@ public class WorkflowManagementFeature : FeatureBase
options.CompressionAlgorithm = CompressionAlgorithm;
options.LogPersistenceMode = LogPersistenceMode;
options.IsReadOnlyMode = IsReadOnlyMode;
});
}
}

View file

@ -66,6 +66,11 @@ public class WorkflowDefinitionFilter
/// </summary>
public bool? IsSystem { get; set; }
/// <summary>
/// Filter on workflows that are read-only.
/// </summary>
public bool? IsReadonly { get; set; }
/// <summary>
/// Applies the filter to the specified queryable.
/// </summary>
@ -85,6 +90,7 @@ public class WorkflowDefinitionFilter
if (filter.UsableAsActivity != null) queryable = queryable.Where(x => x.Options.UsableAsActivity == filter.UsableAsActivity);
if (!string.IsNullOrWhiteSpace(filter.SearchTerm)) queryable = queryable.Where(x => x.Name!.Contains(filter.SearchTerm) || x.Description!.Contains(filter.SearchTerm) || x.Id.Contains(filter.SearchTerm) || x.DefinitionId.Contains(filter.SearchTerm));
if (filter.IsSystem != null) queryable = queryable.Where(x => x.IsSystem == filter.IsSystem);
if (filter.IsReadonly != null) queryable = queryable.Where(x => x.IsReadonly == filter.IsReadonly);
return queryable;
}

View file

@ -62,7 +62,7 @@ public class WorkflowDefinitionMapper
var options = source.Options ?? new WorkflowOptions();
// TODO: Remove this in the future when users have migrated workflows to use the new UsableAsActivity options property.
#pragma warning disable CS0618
options.UsableAsActivity ??= source.UsableAsActivity ?? false;
#pragma warning restore CS0618
@ -88,7 +88,7 @@ public class WorkflowDefinitionMapper
/// <param name="source">The source <see cref="WorkflowDefinition"/>s.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The mapped <see cref="WorkflowDefinitionModel"/>s.</returns>
public async Task<IEnumerable<WorkflowDefinitionModel>> MapAsync(IEnumerable<WorkflowDefinition> source, CancellationToken cancellationToken = default) =>
public async Task<IEnumerable<WorkflowDefinitionModel>> MapAsync(IEnumerable<WorkflowDefinition> source, CancellationToken cancellationToken = default) =>
await Task.WhenAll(source.Select(async x => await MapAsync(x, cancellationToken)));
/// <summary>
@ -124,7 +124,7 @@ public class WorkflowDefinitionMapper
null,
workflow.Root);
}
/// <summary>
/// Maps a <see cref="Workflow"/> to a <see cref="WorkflowDefinitionModel"/>.
/// </summary>

View file

@ -1,4 +1,5 @@
using System.Linq.Expressions;
using Elsa.Models;
using Elsa.Workflows.Management.Entities;
using JetBrains.Annotations;
@ -25,7 +26,8 @@ public class WorkflowDefinitionSummary
IsPublished = workflowDefinition.IsPublished,
ProviderName = workflowDefinition.ProviderName,
MaterializerName = workflowDefinition.MaterializerName,
CreatedAt = workflowDefinition.CreatedAt
CreatedAt = workflowDefinition.CreatedAt,
IsReadonly = workflowDefinition.IsReadonly
};
/// <summary>
@ -43,7 +45,8 @@ public class WorkflowDefinitionSummary
IsPublished = workflowDefinition.IsPublished,
ProviderName = workflowDefinition.ProviderName,
MaterializerName = workflowDefinition.MaterializerName,
CreatedAt = workflowDefinition.CreatedAt
CreatedAt = workflowDefinition.CreatedAt,
IsReadonly = workflowDefinition.IsReadonly
};
/// <summary>

View file

@ -11,7 +11,7 @@ public class ManagementOptions
/// A collection of activity types that are available to the system.
/// </summary>
public HashSet<Type> ActivityTypes { get; set; } = new();
/// <summary>
/// A collection of types that are available to the system as variable types.
/// </summary>
@ -26,4 +26,9 @@ public class ManagementOptions
/// The default Log Persistence Mode to use for all the system
/// </summary>
public LogPersistenceMode LogPersistenceMode { get; set; }
/// <summary>
/// A mode that does not allow editing workflows.
/// </summary>
public bool IsReadOnlyMode { get; set; }
}

View file

@ -67,7 +67,7 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager
{
var definitionIdList = definitionIds.Distinct().ToList();
await _notificationSender.SendAsync(new WorkflowDefinitionsDeleting(definitionIdList), cancellationToken);
var filter = new WorkflowDefinitionFilter { DefinitionIds = definitionIdList };
var filter = new WorkflowDefinitionFilter { DefinitionIds = definitionIdList, IsReadonly = false };
var count = await _store.DeleteAsync(filter, cancellationToken);
await EnsureLastVersionIsLatestAsync(definitionIdList, cancellationToken);
await _notificationSender.SendAsync(new WorkflowDefinitionsDeleted(definitionIdList), cancellationToken);
@ -100,6 +100,28 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager
return await DeleteVersionAsync(definitionToDelete, cancellationToken);
}
/// <inheritdoc />
public async Task<bool> DeleteVersionAsync(WorkflowDefinition definitionToDelete, CancellationToken cancellationToken)
{
if (definitionToDelete.IsPublished)
{
throw new Exception("Published version cannot be deleted before retracting it");
}
await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleting(definitionToDelete), cancellationToken);
var filter = new WorkflowDefinitionFilter { Id = definitionToDelete.Id };
var isDeleted = await _store.DeleteAsync(filter, cancellationToken) > 0;
if (!isDeleted)
return false;
await EnsureLastVersionIsLatestAsync(definitionToDelete.DefinitionId, cancellationToken);
await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleted(definitionToDelete), cancellationToken);
return isDeleted;
}
/// <inheritdoc />
public async Task<WorkflowDefinition> RevertVersionAsync(string definitionId, int version, CancellationToken cancellationToken = default)
{
@ -130,7 +152,7 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager
{
VersionOptions = VersionOptions.LatestOrPublished
}, cancellationToken)).ToList();
// Remove the dependency from the list of workflow definitions to consider.
workflowDefinitions = workflowDefinitions.Where(x => x.DefinitionId != dependency.DefinitionId).ToList();
@ -168,27 +190,6 @@ public class WorkflowDefinitionManager : IWorkflowDefinitionManager
return updatedWorkflowDefinitions;
}
private async Task<bool> DeleteVersionAsync(WorkflowDefinition definitionToDelete, CancellationToken cancellationToken)
{
if (definitionToDelete.IsPublished)
{
throw new Exception("Published version cannot be deleted before retracting it");
}
await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleting(definitionToDelete), cancellationToken);
var filter = new WorkflowDefinitionFilter { Id = definitionToDelete.Id };
var isDeleted = await _store.DeleteAsync(filter, cancellationToken) > 0;
if (!isDeleted)
return false;
await EnsureLastVersionIsLatestAsync(definitionToDelete.DefinitionId, cancellationToken);
await _notificationSender.SendAsync(new WorkflowDefinitionVersionDeleted(definitionToDelete), cancellationToken);
return isDeleted;
}
private async Task EnsureLastVersionIsLatestAsync(IEnumerable<string> definitionIds, CancellationToken cancellationToken)
{
foreach (var definitionId in definitionIds)

View file

@ -10,6 +10,7 @@ using Hangfire.Annotations;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Extensions.DependencyInjection;
using Refit;
using static Elsa.Api.Client.RefitSettingsHelper;
@ -68,6 +69,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
{
management.UseEntityFrameworkCore(ef => ef.UsePostgreSql(dbConnectionString));
management.UseMassTransitDispatcher();
management.UseCache();
});
elsa.UseWorkflowRuntime(runtime =>
{