Add project files.

This commit is contained in:
2025-08-18 17:11:43 +03:30
parent 807dc263be
commit e5eb10d604
3943 changed files with 223719 additions and 0 deletions
@@ -0,0 +1,47 @@
using Elsa.Abstractions;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityDescriptorOptions.Get;
[PublicAPI]
internal class Get : ElsaEndpoint<Request, Response>
{
private readonly IServiceProvider _serviceProvider;
private readonly IActivityRegistryLookupService _registryLookup;
private readonly IPropertyUIHandlerResolver _optionsResolver;
/// <inheritdoc />
public Get(IServiceProvider serviceProvider, IActivityRegistryLookupService registryLookup, IPropertyUIHandlerResolver optionsResolver)
{
_serviceProvider = serviceProvider;
_registryLookup = registryLookup;
_optionsResolver = optionsResolver;
}
/// <inheritdoc />
public override void Configure()
{
Post("/descriptors/activities/{activityTypeName}/options/{propertyName}");
ConfigurePermissions("read:*", "read:activity-descriptors-options");
}
/// <inheritdoc />
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var descriptor = request.Version == null ? await _registryLookup.FindAsync(request.ActivityTypeName) : await _registryLookup.FindAsync(request.ActivityTypeName, request.Version.Value);
if (descriptor == null)
await SendNotFoundAsync(cancellationToken);
var propertyDescriptor = descriptor!.Inputs.FirstOrDefault(x => x.Name == request.PropertyName);
if(propertyDescriptor == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var inputOptions = await _optionsResolver.GetUIPropertiesAsync(propertyDescriptor.PropertyInfo!, request.Context, cancellationToken);
await SendOkAsync(new Response(inputOptions), cancellationToken);
}
}
@@ -0,0 +1,19 @@
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityDescriptorOptions.Get;
internal class Request
{
public string ActivityTypeName { get; set; } = default!;
public int? Version { get; set; }
public string PropertyName { get; set; } = default!;
public object? Context { get; set; }
}
[PublicAPI]
internal class Response(IDictionary<string, object> items)
{
public IDictionary<string, object> Items { get; set; } = items;
}
@@ -0,0 +1,35 @@
using Elsa.Abstractions;
using Elsa.Workflows.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityDescriptors.Get;
[PublicAPI]
internal class Get : ElsaEndpoint<Request, ActivityDescriptor>
{
private readonly IActivityRegistryLookupService _registryLookup;
/// <inheritdoc />
public Get(IActivityRegistryLookupService registryLookup)
{
_registryLookup = registryLookup;
}
/// <inheritdoc />
public override void Configure()
{
Get("/descriptors/activities/{typeName}");
ConfigurePermissions("read:*", "read:activity-descriptors");
}
/// <inheritdoc />
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var descriptor = request.Version == null ? await _registryLookup.FindAsync(request.TypeName) : await _registryLookup.FindAsync(request.TypeName, request.Version.Value);
if (descriptor == null)
await SendNotFoundAsync(cancellationToken);
else
await SendOkAsync(descriptor, cancellationToken);
}
}
@@ -0,0 +1,7 @@
namespace Elsa.Workflows.Api.Endpoints.ActivityDescriptors.Get;
internal class Request
{
public string TypeName { get; set; }
public int? Version { get; set; }
}
@@ -0,0 +1,28 @@
using Elsa.Abstractions;
using Elsa.Workflows.Management;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityDescriptors.List;
[PublicAPI]
internal class List(IActivityRegistry registry, IActivityRegistryPopulator registryPopulator) : ElsaEndpointWithoutRequest<Response>
{
public override void Configure()
{
Get("/descriptors/activities");
ConfigurePermissions("read:*", "read:activity-descriptors");
}
public override async Task<Response> ExecuteAsync(CancellationToken cancellationToken)
{
var forceRefresh = Query<bool>("refresh", false);
if (forceRefresh)
await registryPopulator.PopulateRegistryAsync(cancellationToken);
var descriptors = registry.ListAll().ToList();
var response = new Response(descriptors);
return response;
}
}
@@ -0,0 +1,11 @@
using Elsa.Workflows.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityDescriptors.List;
[PublicAPI]
internal class Response(ICollection<ActivityDescriptor> items)
{
public ICollection<ActivityDescriptor> Items { get; set; } = items;
public int Count => Items.Count;
}
@@ -0,0 +1,38 @@
using Elsa.Abstractions;
using Elsa.Common.Entities;
using Elsa.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.OrderDefinitions;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutionSummaries.ListSummaries;
/// <summary>
/// Lists a summary view of the executions for a given activity.
/// </summary>
[PublicAPI]
internal class Endpoint(IActivityExecutionStore store) : ElsaEndpoint<Request, ListResponse<ActivityExecutionRecordSummary>>
{
/// <inheritdoc />
public override void Configure()
{
Get("/activity-execution-summaries/list");
ConfigurePermissions("read:activity-execution");
}
/// <inheritdoc />
public override async Task<ListResponse<ActivityExecutionRecordSummary>> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var filter = new ActivityExecutionRecordFilter
{
WorkflowInstanceId = request.WorkflowInstanceId,
ActivityNodeId = request.ActivityNodeId,
Completed = request.Completed
};
var order = new ActivityExecutionRecordOrder<DateTimeOffset>(x => x.StartedAt, OrderDirection.Ascending);
var records = (await store.FindManySummariesAsync(filter, order, cancellationToken)).ToList();
return new ListResponse<ActivityExecutionRecordSummary>(records);
}
}
@@ -0,0 +1,22 @@
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutionSummaries.ListSummaries;
/// <summary>
/// A request for a list of activity execution log record summaries for a given activity in a workflow instance.
/// </summary>
internal class Request
{
/// <summary>
/// The ID of the workflow instance to get the execution log for.
/// </summary>
public string WorkflowInstanceId { get; set; } = default!;
/// <summary>
/// The node ID of the activity to get the execution record for.
/// </summary>
public string ActivityNodeId { get; set; } = default!;
/// <summary>
/// Whether to include completed activity execution records. If not specified, all activity execution records will be included.
/// </summary>
public bool? Completed { get; set; }
}
@@ -0,0 +1,42 @@
using Elsa.Abstractions;
using Elsa.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Filters;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.Count;
/// <summary>
/// Counts the number of executions for a given activity.
/// </summary>
[PublicAPI]
internal class Count : ElsaEndpoint<Request, CountResponse>
{
private readonly IActivityExecutionStore _store;
/// <inheritdoc />
public Count(IActivityExecutionStore store)
{
_store = store;
}
/// <inheritdoc />
public override void Configure()
{
Get("/activity-executions/count");
ConfigurePermissions("read:activity-execution");
}
/// <inheritdoc />
public override async Task<CountResponse> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var filter = new ActivityExecutionRecordFilter
{
WorkflowInstanceId = request.WorkflowInstanceId,
ActivityId = request.ActivityId,
Completed = request.Completed
};
var count = await _store.CountAsync(filter, cancellationToken);
return new CountResponse(count);
}
}
@@ -0,0 +1,22 @@
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.Count;
/// <summary>
/// Represents a request for list of activity execution log records for a given activity in a workflow instance.
/// </summary>
internal class Request
{
/// <summary>
/// The ID of the workflow instance to get the execution log for.
/// </summary>
public string WorkflowInstanceId { get; set; } = default!;
/// <summary>
/// The ID of the activity to get the execution record for.
/// </summary>
public string ActivityId { get; set; } = default!;
/// <summary>
/// Whether to include completed activity execution records. If not specified, all activity execution records will be included.
/// </summary>
public bool? Completed { get; set; }
}
@@ -0,0 +1,41 @@
using Elsa.Abstractions;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.Get;
/// <summary>
/// Gets an individual execution for a given activity.
/// </summary>
[PublicAPI]
internal class Endpoint(IActivityExecutionStore store) : ElsaEndpointWithoutRequest<ActivityExecutionRecord>
{
/// <inheritdoc />
public override void Configure()
{
Get("/activity-executions/{id}");
ConfigurePermissions("read:activity-execution");
}
/// <inheritdoc />
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var id = Route<string>("id");
var filter = new ActivityExecutionRecordFilter
{
Id = id
};
var record = await store.FindAsync(filter, cancellationToken);
if (record == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(record, cancellationToken);
}
}
@@ -0,0 +1,46 @@
using Elsa.Abstractions;
using Elsa.Common.Entities;
using Elsa.Models;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.OrderDefinitions;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.List;
/// <summary>
/// Lists the executions for a given activity.
/// </summary>
[PublicAPI]
internal class List : ElsaEndpoint<Request, ListResponse<ActivityExecutionRecord>>
{
private readonly IActivityExecutionStore _store;
/// <inheritdoc />
public List(IActivityExecutionStore store)
{
_store = store;
}
/// <inheritdoc />
public override void Configure()
{
Get("/activity-executions/list");
ConfigurePermissions("read:activity-execution");
}
/// <inheritdoc />
public override async Task<ListResponse<ActivityExecutionRecord>> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var filter = new ActivityExecutionRecordFilter
{
WorkflowInstanceId = request.WorkflowInstanceId,
ActivityNodeId = request.ActivityNodeId,
Completed = request.Completed
};
var order = new ActivityExecutionRecordOrder<DateTimeOffset>(x => x.StartedAt, OrderDirection.Ascending);
var records = (await _store.FindManyAsync(filter, order, cancellationToken)).ToList();
return new ListResponse<ActivityExecutionRecord>(records);
}
}
@@ -0,0 +1,22 @@
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.List;
/// <summary>
/// Represents a request for list of activity execution log records for a given activity in a workflow instance.
/// </summary>
internal class Request
{
/// <summary>
/// The ID of the workflow instance to get the execution log for.
/// </summary>
public string WorkflowInstanceId { get; set; } = default!;
/// <summary>
/// The node ID of the activity to get the execution record for.
/// </summary>
public string ActivityNodeId { get; set; } = default!;
/// <summary>
/// Whether to include completed activity execution records. If not specified, all activity execution records will be included.
/// </summary>
public bool? Completed { get; set; }
}
@@ -0,0 +1,40 @@
using Elsa.Abstractions;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.Report;
/// <summary>
/// Gets a report for the specified set of activities that includes number of executions, split by completed and non-completed executions as well as blocking activities.
/// </summary>
[PublicAPI]
internal class Report : ElsaEndpoint<Request, Response>
{
private readonly IActivityExecutionStore _store;
private readonly IActivityExecutionStatsService _activityExecutionStatsService;
/// <inheritdoc />
public Report(IActivityExecutionStore store, IActivityExecutionStatsService activityExecutionStatsService)
{
_store = store;
_activityExecutionStatsService = activityExecutionStatsService;
}
/// <inheritdoc />
public override void Configure()
{
Post("/activity-executions/report");
ConfigurePermissions("read:activity-execution");
}
/// <inheritdoc />
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var stats = (await _activityExecutionStatsService.GetStatsAsync(request.WorkflowInstanceId, request.ActivityNodeIds, cancellationToken)).ToList();
return new()
{
Stats = stats
};
}
}
@@ -0,0 +1,24 @@
using Elsa.Workflows.Runtime;
namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.Report;
/// <summary>
/// Represents a request for a list of activity execution log records for a given activity in a workflow instance.
/// </summary>
internal class Request
{
/// <summary>
/// The ID of the workflow instance to get the execution log for.
/// </summary>
public string WorkflowInstanceId { get; set; } = null!;
/// <summary>
/// The node IDs of the activities to get the execution records for.
/// </summary>
public ICollection<string> ActivityNodeIds { get; set; } = null!;
}
internal class Response
{
public ICollection<ActivityExecutionStats> Stats { get; set; } = new List<ActivityExecutionStats>();
}
@@ -0,0 +1,89 @@
using Elsa.Abstractions;
using Elsa.SasTokens.Contracts;
using Elsa.Workflows.Runtime;
using FastEndpoints;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.Bookmarks.Resume;
/// <summary>
/// Resumes a bookmarked workflow instance with the bookmark ID specified in the provided SAS token.
/// </summary>
[PublicAPI]
internal class Resume(ITokenService tokenService, IBookmarkQueue bookmarkQueue, IPayloadSerializer serializer) : ElsaEndpoint<Request>
{
/// <inheritdoc />
public override void Configure()
{
Routes("/bookmarks/resume");
Verbs(Http.GET, Http.POST);
AllowAnonymous();
}
/// <inheritdoc />
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var token = Query<string>("t")!;
if (!tokenService.TryDecryptToken<BookmarkTokenPayload>(token, out var payload))
AddError("Invalid token.");
var input = HttpContext.Request.Method == HttpMethods.Post ? request.Input : GetInputFromQueryString();
if (ValidationFailed)
{
await SendErrorsAsync(cancellation: cancellationToken);
return;
}
await ResumeBookmarkedWorkflowAsync(payload, input, cancellationToken);
if (!HttpContext.Response.HasStarted)
await SendOkAsync(cancellationToken);
}
private IDictionary<string, object>? GetInputFromQueryString()
{
var inputJson = Query<string?>("in", false);
if (string.IsNullOrWhiteSpace(inputJson))
return null;
try
{
return serializer.Deserialize<IDictionary<string, object>>(inputJson);
}
catch
{
AddError("Invalid input format. Expected a valid JSON string.");
return null;
}
}
private async Task ResumeBookmarkedWorkflowAsync(BookmarkTokenPayload tokenPayload, IDictionary<string, object>? input, CancellationToken cancellationToken)
{
var bookmarkId = tokenPayload.BookmarkId;
var workflowInstanceId = tokenPayload.WorkflowInstanceId;
var item = new NewBookmarkQueueItem
{
BookmarkId = bookmarkId,
WorkflowInstanceId = workflowInstanceId,
Options = new()
{
Input = input
}
};
await bookmarkQueue.EnqueueAsync(item, cancellationToken);
}
}
/// <summary>
/// The request model for the Resume endpoint.
/// </summary>
internal class Request
{
/// <summary>
/// The input to provide to the workflow when resuming.
/// </summary>
public IDictionary<string, object>? Input { get; set; }
}
@@ -0,0 +1,29 @@
using Elsa.Abstractions;
using Elsa.Models;
using Elsa.Workflows.CommitStates;
namespace Elsa.Workflows.Api.Endpoints.CommitStrategies.Activities.List;
/// <summary>
/// Represents an API endpoint that provides a list of registered workflow commit strategies.
/// </summary>
/// <remarks>
/// This class is an implementation of an endpoint that retrieves a collection of workflow commit strategy registrations
/// from a provided registry and returns them in a unified response.
/// </remarks>
internal class List(ICommitStrategyRegistry registry) : ElsaEndpointWithoutRequest<ListResponse<CommitStrategyDescriptor>>
{
public override void Configure()
{
Get("/descriptors/commit-strategies/activities");
ConfigurePermissions("read:commit-strategies");
}
public override Task<ListResponse<CommitStrategyDescriptor>> ExecuteAsync(CancellationToken cancellationToken)
{
var registrations = registry.ListActivityStrategyRegistrations().ToList();
var descriptors = CommitStrategyDescriptor.FromStrategyMetadata(registrations.Select(x => x.Metadata)).ToList();
var response =new ListResponse<CommitStrategyDescriptor>(descriptors);
return Task.FromResult(response);
}
}
@@ -0,0 +1,16 @@
using Elsa.Workflows.CommitStates;
namespace Elsa.Workflows.Api.Endpoints.CommitStrategies;
internal record CommitStrategyDescriptor(string Name, string DisplayName, string Description)
{
public static CommitStrategyDescriptor FromStrategyMetadata(CommitStrategyMetadata metadata)
{
return new(metadata.Name, metadata.DisplayName, metadata.Description);
}
public static IEnumerable<CommitStrategyDescriptor> FromStrategyMetadata(IEnumerable<CommitStrategyMetadata> metadata)
{
return metadata.Select(FromStrategyMetadata);
}
}
@@ -0,0 +1,29 @@
using Elsa.Abstractions;
using Elsa.Models;
using Elsa.Workflows.CommitStates;
namespace Elsa.Workflows.Api.Endpoints.CommitStrategies.Workflows.List;
/// <summary>
/// Represents an API endpoint that provides a list of registered workflow commit strategies.
/// </summary>
/// <remarks>
/// This class is an implementation of an endpoint that retrieves a collection of workflow commit strategy registrations
/// from a provided registry and returns them in a unified response.
/// </remarks>
internal class List(ICommitStrategyRegistry registry) : ElsaEndpointWithoutRequest<ListResponse<CommitStrategyDescriptor>>
{
public override void Configure()
{
Get("/descriptors/commit-strategies/workflows");
ConfigurePermissions("read:commit-strategies");
}
public override Task<ListResponse<CommitStrategyDescriptor>> ExecuteAsync(CancellationToken cancellationToken)
{
var registrations = registry.ListWorkflowStrategyRegistrations().ToList();
var descriptors = CommitStrategyDescriptor.FromStrategyMetadata(registrations.Select(x => x.Metadata)).ToList();
var response =new ListResponse<CommitStrategyDescriptor>(descriptors);
return Task.FromResult(response);
}
}
@@ -0,0 +1,52 @@
using Elsa.Abstractions;
using Elsa.Workflows.Models;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.Events.TriggerAuthenticated;
/// <summary>
/// Triggers all workflows that are waiting for the specified event.
/// </summary>
[PublicAPI]
internal class Trigger : ElsaEndpoint<Request>
{
private readonly IEventPublisher _eventPublisher;
/// <inheritdoc />
public Trigger(IEventPublisher eventPublisher)
{
_eventPublisher = eventPublisher;
}
/// <inheritdoc />
public override void Configure()
{
Post("/events/{eventName}/trigger");
ConfigurePermissions("trigger:event");
}
/// <inheritdoc />
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var input = (IDictionary<string, object>?)request.Input;
var eventName = request.EventName;
var correlationId = request.CorrelationId;
var workflowInstanceId = request.WorkflowInstanceId;
var activityInstanceId = request.ActivityInstanceId;
var workflowExecutionMode = request.WorkflowExecutionMode;
if (workflowExecutionMode == WorkflowExecutionMode.Asynchronous)
{
await _eventPublisher.PublishAsync(eventName, correlationId, workflowInstanceId, activityInstanceId, input, true, cancellationToken);
await SendOkAsync(cancellationToken);
}
else
{
await _eventPublisher.PublishAsync(eventName, correlationId, workflowInstanceId, activityInstanceId, input, false, cancellationToken);
if (!HttpContext.Response.HasStarted)
await SendOkAsync(cancellationToken);
}
}
}
@@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
using Elsa.Workflows.Models;
using Elsa.Workflows.Serialization.Converters;
namespace Elsa.Workflows.Api.Endpoints.Events.TriggerAuthenticated;
internal class Request
{
public string EventName { get; set; } = default!;
public string? WorkflowInstanceId { get; set; }
public string? CorrelationId { get; set; }
public string? ActivityInstanceId { get; set; }
[JsonConverter(typeof(ExpandoObjectConverterFactory))]
public object? Input { get; set; }
public WorkflowExecutionMode WorkflowExecutionMode { get; set; } = WorkflowExecutionMode.Asynchronous;
}
@@ -0,0 +1,50 @@
using Elsa.Abstractions;
using Elsa.SasTokens.Contracts;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.Events.TriggerPublic;
/// <summary>
/// Resumes a workflow instance blocked by a specified event encoded in the provided SAS token.
/// </summary>
[PublicAPI]
internal class Trigger : ElsaEndpointWithoutRequest
{
private readonly ITokenService _tokenService;
private readonly IEventPublisher _eventPublisher;
/// <inheritdoc />
public Trigger(ITokenService tokenService, IEventPublisher eventPublisher)
{
_tokenService = tokenService;
_eventPublisher = eventPublisher;
}
/// <inheritdoc />
public override void Configure()
{
Get("/events/trigger");
AllowAnonymous();
}
/// <inheritdoc />
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var token = Query<string>("t")!;
if (!_tokenService.TryDecryptToken<EventTokenPayload>(token, out var payload))
{
AddError("Invalid token.");
await SendErrorsAsync(cancellation: cancellationToken);
return;
}
var eventName = payload.EventName;
var workflowInstanceId = payload.WorkflowInstanceId;
await _eventPublisher.PublishAsync(eventName, workflowInstanceId: workflowInstanceId, cancellationToken: cancellationToken);
if (!HttpContext.Response.HasStarted)
await SendOkAsync(cancellationToken);
}
}
@@ -0,0 +1,43 @@
using Elsa.Abstractions;
using Elsa.Features.Contracts;
using Elsa.Features.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.Features.Get;
/// <summary>
/// Returns the specified installed feature.
/// </summary>
[PublicAPI]
internal class Get : ElsaEndpointWithoutRequest<FeatureDescriptor>
{
private readonly IInstalledFeatureRegistry _installedFeatureRegistry;
/// <inheritdoc />
public Get(IInstalledFeatureRegistry installedFeatureRegistry)
{
_installedFeatureRegistry = installedFeatureRegistry;
}
/// <inheritdoc />
public override void Configure()
{
Get("/features/installed/{fullName}");
ConfigurePermissions("read:*", "read:installed-features");
}
/// <inheritdoc />
public override async Task HandleAsync( CancellationToken cancellationToken)
{
var fullName = Route<string>("fullName")!;
var descriptor = _installedFeatureRegistry.Find(fullName);
if (descriptor == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(descriptor, cancellationToken);
}
}
@@ -0,0 +1,38 @@
using Elsa.Abstractions;
using Elsa.Features.Contracts;
using Elsa.Features.Models;
using Elsa.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.Features.List;
/// <summary>
/// Returns a list of installed features.
/// </summary>
[PublicAPI]
internal class List : ElsaEndpointWithoutRequest<ListResponse<FeatureDescriptor>>
{
private readonly IInstalledFeatureRegistry _installedFeatureRegistry;
/// <inheritdoc />
public List(IInstalledFeatureRegistry installedFeatureRegistry)
{
_installedFeatureRegistry = installedFeatureRegistry;
}
/// <inheritdoc />
public override void Configure()
{
Get("/features/installed");
ConfigurePermissions("read:*", "read:installed-features");
}
/// <inheritdoc />
public override Task<ListResponse<FeatureDescriptor>> ExecuteAsync(CancellationToken cancellationToken)
{
var descriptors = _installedFeatureRegistry.List().ToList();
var response = new ListResponse<FeatureDescriptor>(descriptors);
return Task.FromResult(response);
}
}
@@ -0,0 +1,43 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Elsa.Abstractions;
using Elsa.Extensions;
using Elsa.Models;
using Humanizer;
namespace Elsa.Workflows.Api.Endpoints.IncidentStrategies.List;
/// <summary>
/// Returns list of available <see cref="IIncidentStrategy" /> implementations.
/// </summary>
internal class List(IEnumerable<IIncidentStrategy> strategies) : ElsaEndpointWithoutRequest<ListResponse<IncidentStrategyDescriptor>>
{
public override void Configure()
{
Get("/descriptors/incident-strategies");
ConfigurePermissions("read:incident-strategies");
}
public override Task<ListResponse<IncidentStrategyDescriptor>> ExecuteAsync(CancellationToken cancellationToken)
{
var descriptors = strategies.Select(IncidentStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
var response =new ListResponse<IncidentStrategyDescriptor>(descriptors);
return Task.FromResult(response);
}
}
internal record IncidentStrategyDescriptor(string DisplayName, string Description, string TypeName)
{
public static IncidentStrategyDescriptor FromStrategy(IIncidentStrategy strategy)
{
var type = strategy.GetType();
var displayNameAttribute = type.GetCustomAttribute<DisplayNameAttribute>();
var descriptionAttribute = type.GetCustomAttribute<DescriptionAttribute>();
var displayAttribute = type.GetCustomAttribute<DisplayAttribute>();
var displayName = displayNameAttribute?.DisplayName ?? displayAttribute?.Name ?? type.Name.Replace("Strategy", "").Humanize();
var description = descriptionAttribute?.Description ?? displayAttribute?.Description ?? "";
return new IncidentStrategyDescriptor(displayName, description, type.GetSimpleAssemblyQualifiedName());
}
}
@@ -0,0 +1,45 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Elsa.Abstractions;
using Elsa.Extensions;
using Elsa.Models;
using Elsa.Workflows.LogPersistence;
using Humanizer;
namespace Elsa.Workflows.Api.Endpoints.LogPersistenceStrategies.List;
/// <summary>
/// Returns list of available <see cref="ILogPersistenceStrategy" /> implementations.
/// </summary>
internal class Endpoint(ILogPersistenceStrategyService logPersistenceStrategyService) : ElsaEndpointWithoutRequest<ListResponse<LogPersistenceStrategyDescriptor>>
{
public override void Configure()
{
Get("/descriptors/log-persistence-strategies");
ConfigurePermissions("read:log-persistence-strategies");
}
public override Task<ListResponse<LogPersistenceStrategyDescriptor>> ExecuteAsync(CancellationToken cancellationToken)
{
var strategies = logPersistenceStrategyService.ListStrategies();
var descriptors = strategies.Select(LogPersistenceStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
var response =new ListResponse<LogPersistenceStrategyDescriptor>(descriptors);
return Task.FromResult(response);
}
}
internal record LogPersistenceStrategyDescriptor(string DisplayName, string Description, string TypeName)
{
public static LogPersistenceStrategyDescriptor FromStrategy(ILogPersistenceStrategy strategy)
{
var type = strategy.GetType();
var displayNameAttribute = type.GetCustomAttribute<DisplayNameAttribute>();
var descriptionAttribute = type.GetCustomAttribute<DescriptionAttribute>();
var displayAttribute = type.GetCustomAttribute<DisplayAttribute>();
var displayName = displayNameAttribute?.DisplayName ?? displayAttribute?.Name ?? type.Name.Replace("Strategy", "").Humanize();
var description = descriptionAttribute?.Description ?? displayAttribute?.Description ?? "";
return new LogPersistenceStrategyDescriptor(displayName, description, type.GetSimpleAssemblyQualifiedName());
}
}
@@ -0,0 +1,30 @@
using System.Reflection;
using FastEndpoints;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.Package;
/// <summary>
/// Returns the package informational version.
/// </summary>
[PublicAPI]
internal class Version : EndpointWithoutRequest<Response>
{
public override void Configure()
{
Get("/package/version");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var assembly = Assembly.GetExecutingAssembly();
var versionAttribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
var version = RemoveAutoGeneratedPostfix(versionAttribute);
await SendOkAsync(new Response(version), cancellationToken);
}
private static string RemoveAutoGeneratedPostfix(AssemblyInformationalVersionAttribute? versionAttribute) =>
versionAttribute?.InformationalVersion.Split("+")[0]!;
}
@@ -0,0 +1,3 @@
namespace Elsa.Workflows.Api.Endpoints.Package;
internal record Response(string PackageVersion);
@@ -0,0 +1,45 @@
using Elsa.Abstractions;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Models;
using Elsa.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.Scripting.ExpressionDescriptors.List;
/// <summary>
/// Returns a TypeScript definition that is used by the Monaco editor to display intellisense for JavaScript expressions.
/// </summary>
[UsedImplicitly]
internal class List(IExpressionDescriptorRegistry expressionDescriptorRegistry) : ElsaEndpointWithoutRequest<ListResponse<ExpressionDescriptorModel>>
{
/// <inheritdoc />
public override void Configure()
{
Get("/descriptors/expression-descriptors");
ConfigurePermissions("read:*", "read:expression-descriptors");
}
/// <inheritdoc />
public override Task HandleAsync(CancellationToken cancellationToken)
{
var descriptors = expressionDescriptorRegistry.ListAll().ToList();
var models = Map(descriptors).ToList();
var response = new ListResponse<ExpressionDescriptorModel>(models);
return SendOkAsync(response, cancellationToken);
}
private static IEnumerable<ExpressionDescriptorModel> Map(List<ExpressionDescriptor> descriptors) => descriptors.Select(Map);
private static ExpressionDescriptorModel Map(ExpressionDescriptor descriptor)
{
var properties = descriptor.Properties;
return new ExpressionDescriptorModel(
descriptor.Type,
descriptor.DisplayName,
descriptor.IsSerializable,
descriptor.IsBrowsable,
properties);
}
}
internal record ExpressionDescriptorModel(string Type, string DisplayName, bool IsSerializable, bool IsBrowsable, IDictionary<string, object> Properties);
@@ -0,0 +1,13 @@
namespace Elsa.Workflows.Api.Endpoints.StorageDrivers.List;
/// <summary>
/// Provides policy names accepted by the <see cref="List"/> endpoint.
/// </summary>
public static class Constants
{
/// <summary>
/// The policy name accepted by this endpoint.
/// </summary>
public const string PolicyName = "ListStorageDrivers";
}
@@ -0,0 +1,47 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Elsa.Abstractions;
using Elsa.Extensions;
namespace Elsa.Workflows.Api.Endpoints.StorageDrivers.List;
/// <summary>
/// Returns a list of registered <see cref="IStorageDriver"/> implementations.
/// </summary>
public class List : ElsaEndpointWithoutRequest<Response>
{
private readonly IStorageDriverManager _registry;
/// <inheritdoc />
public List(IStorageDriverManager registry)
{
_registry = registry;
}
/// <inheritdoc />
public override void Configure()
{
Get("/descriptors/storage-drivers");
ConfigurePermissions("read:*", "read:storage-drivers");
}
/// <inheritdoc />
public override Task<Response> ExecuteAsync(CancellationToken ct)
{
var drivers = _registry.List();
var descriptors = drivers.Select(FromDriver).OrderByDescending(x => x.Priority).ToList();
var response = new Response(descriptors);
return Task.FromResult(response);
}
private static StorageDriverDescriptor FromDriver(IStorageDriver driver)
{
var type = driver.GetType();
var deprecated = type.GetCustomAttribute<ObsoleteAttribute>() != null;
var displayName = type.GetCustomAttribute<DisplayAttribute>()?.Name ?? type.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? type.Name.Replace("StorageDriver", "");
var priority = driver.Priority;
return new StorageDriverDescriptor(type.GetSimpleAssemblyQualifiedName(), displayName, priority, deprecated);
}
}
@@ -0,0 +1,8 @@
namespace Elsa.Workflows.Api.Endpoints.StorageDrivers.List;
public class Response(ICollection<StorageDriverDescriptor> items)
{
public ICollection<StorageDriverDescriptor> Items { get; set; } = items;
}
public record StorageDriverDescriptor(string TypeName, string DisplayName, double Priority = 0, bool Deprecated = false);
@@ -0,0 +1,13 @@
namespace Elsa.Workflows.Api.Endpoints.Tasks.Complete;
/// <summary>
/// Provides policy names accepted by the <see cref="Complete"/> endpoint.
/// </summary>
public static class Constants
{
/// <summary>
/// The policy name accepted by this endpoint.
/// </summary>
public const string PolicyName = "CompleteTask";
}
@@ -0,0 +1,33 @@
using Elsa.Abstractions;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.Workflows.Api.Endpoints.Tasks.Complete;
/// <summary>
/// Resumes the <see cref="RunTask"/> activity matching the received Task ID.
/// </summary>
public class Complete : ElsaEndpoint<Request, Response>
{
private readonly ITaskReporter _taskReporter;
/// <inheritdoc />
public Complete(ITaskReporter taskReporter)
{
_taskReporter = taskReporter;
}
/// <inheritdoc />
public override void Configure()
{
Post("/tasks/{taskId}/complete");
ConfigurePermissions("tasks:complete");
}
/// <inheritdoc />
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
await _taskReporter.ReportCompletionAsync(request.TaskId, request.Result, cancellationToken);
if (!HttpContext.Response.HasStarted) await SendOkAsync(cancellationToken);
}
}
@@ -0,0 +1,11 @@
namespace Elsa.Workflows.Api.Endpoints.Tasks.Complete;
public class Request
{
public string TaskId { get; set; } = default!;
public object? Result { get; set; }
}
public class Response
{
}
@@ -0,0 +1,56 @@
using System.ComponentModel;
using System.Reflection;
using Elsa.Abstractions;
using Elsa.Expressions.Contracts;
using Elsa.Extensions;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Management.Options;
using JetBrains.Annotations;
using Microsoft.Extensions.Options;
namespace Elsa.Workflows.Api.Endpoints.VariableTypes.List;
/// <summary>
/// Returns a list of available variable types.
/// </summary>
[PublicAPI]
internal class List : ElsaEndpointWithoutRequest<Response>
{
private readonly IWellKnownTypeRegistry _wellKnownTypeRegistry;
private readonly ManagementOptions _managementOptions;
/// <inheritdoc />
public List(IOptions<ManagementOptions> managementOptions, IWellKnownTypeRegistry wellKnownTypeRegistry)
{
_wellKnownTypeRegistry = wellKnownTypeRegistry;
_managementOptions = managementOptions.Value;
}
/// <inheritdoc />
public override void Configure()
{
Get("/descriptors/variables");
ConfigurePermissions("read:*", "read:variable-descriptors");
}
/// <inheritdoc />
public override Task<Response> ExecuteAsync(CancellationToken cancellationToken)
{
var types = _managementOptions.VariableDescriptors;
var descriptors = types.Select(Map).ToList();
var response = new Response(descriptors);
return Task.FromResult(response);
}
private VariableTypeDescriptor Map(VariableDescriptor descriptor)
{
var type = descriptor.Type;
var hasAlias = _wellKnownTypeRegistry.TryGetAlias(type, out var alias);
var typeName = hasAlias ? alias : type.GetSimpleAssemblyQualifiedName()!;
var displayName = hasAlias ? alias : type.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? type.Name;
var description = descriptor.Description ?? type.GetCustomAttribute<DescriptionAttribute>()?.Description;
return new(typeName, displayName, descriptor.Category, description);
}
}
@@ -0,0 +1,8 @@
namespace Elsa.Workflows.Api.Endpoints.VariableTypes.List;
internal class Response(ICollection<VariableTypeDescriptor> items)
{
public ICollection<VariableTypeDescriptor> Items { get; set; } = items;
}
internal record VariableTypeDescriptor(string TypeName, string DisplayName, string Category, string? Description);
@@ -0,0 +1,43 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Elsa.Abstractions;
using Elsa.Extensions;
using Elsa.Models;
using Humanizer;
namespace Elsa.Workflows.Api.Endpoints.WorkflowActivationStrategies.List;
/// <summary>
/// Returns list of available <see cref="IWorkflowActivationStrategy" /> implementations.
/// </summary>
internal class List(IEnumerable<IWorkflowActivationStrategy> strategies) : ElsaEndpointWithoutRequest<ListResponse<WorkflowActivationStrategyDescriptor>>
{
public override void Configure()
{
Get("/descriptors/workflow-activation-strategies");
ConfigurePermissions("read:workflow-activation-strategies");
}
public override Task<ListResponse<WorkflowActivationStrategyDescriptor>> ExecuteAsync(CancellationToken cancellationToken)
{
var descriptors = strategies.Select(WorkflowActivationStrategyDescriptor.FromStrategy).OrderBy(x => x.DisplayName).ToList();
var response =new ListResponse<WorkflowActivationStrategyDescriptor>(descriptors);
return Task.FromResult(response);
}
}
internal record WorkflowActivationStrategyDescriptor(string DisplayName, string Description, string TypeName)
{
public static WorkflowActivationStrategyDescriptor FromStrategy(IWorkflowActivationStrategy strategy)
{
var type = strategy.GetType();
var displayNameAttribute = type.GetCustomAttribute<DisplayNameAttribute>();
var descriptionAttribute = type.GetCustomAttribute<DescriptionAttribute>();
var displayAttribute = type.GetCustomAttribute<DisplayAttribute>();
var displayName = displayNameAttribute?.DisplayName ?? displayAttribute?.Name ?? type.Name.Replace("Strategy", "").Humanize();
var description = descriptionAttribute?.Description ?? displayAttribute?.Description ?? "";
return new WorkflowActivationStrategyDescriptor(displayName, description, type.GetSimpleAssemblyQualifiedName());
}
}
@@ -0,0 +1,33 @@
using Elsa.Abstractions;
using Elsa.Workflows.Api.Constants;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDelete;
[UsedImplicitly]
internal class BulkDelete(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
public override void Configure()
{
Post("/bulk-actions/delete/workflow-definitions/by-definition-id");
ConfigurePermissions("delete:workflow-definitions");
}
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return null!;
}
var count = await workflowDefinitionManager.BulkDeleteByDefinitionIdsAsync(request.DefinitionIds, cancellationToken);
return new Response(count);
}
}
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDelete;
internal class Request
{
public ICollection<string> DefinitionIds { get; set; } = default!;
}
internal class Response(long deletedCount)
{
[JsonPropertyName("deleted")] public long DeletedCount { get; } = deletedCount;
}
@@ -0,0 +1,33 @@
using Elsa.Abstractions;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDeleteVersions;
[PublicAPI]
internal class BulkDeleteVersions(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
public override void Configure()
{
Post("/bulk-actions/delete/workflow-definitions/by-id");
ConfigurePermissions("delete:workflow-definitions");
}
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return null!;
}
var count = await workflowDefinitionManager.BulkDeleteByIdsAsync(request.Ids, cancellationToken);
return new Response(count);
}
}
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDeleteVersions;
internal class Request
{
public ICollection<string> Ids { get; set; } = default!;
}
internal class Response(long deletedCount)
{
[JsonPropertyName("deleted")] public long DeletedCount { get; } = deletedCount;
}
@@ -0,0 +1,53 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Management;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Requests;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDispatch;
[PublicAPI]
internal class Endpoint(IWorkflowDefinitionService workflowDefinitionService, IWorkflowDispatcher workflowDispatcher, IIdentityGenerator identityGenerator)
: ElsaEndpoint<Request, Response>
{
public override void Configure()
{
Post("/workflow-definitions/{definitionId}/bulk-dispatch");
ConfigurePermissions("exec:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var definitionId = request.DefinitionId;
var versionOptions = request.VersionOptions ?? VersionOptions.Published;
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, versionOptions, cancellationToken);
if (workflowGraph == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var instanceIds = new List<string>();
for (var i = 0; i < request.Count; i++)
{
var instanceId = identityGenerator.GenerateId();
var triggerActivityId = request.TriggerActivityId;
var input = (IDictionary<string, object>?)request.Input;
var dispatchRequest = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id)
{
Input = input,
InstanceId = instanceId,
TriggerActivityId = triggerActivityId
};
await workflowDispatcher.DispatchAsync(dispatchRequest, cancellationToken: cancellationToken);
instanceIds.Add(instanceId);
}
await SendOkAsync(new Response(instanceIds), cancellationToken);
}
}
@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
using Elsa.Common.Models;
using Elsa.Workflows.Serialization.Converters;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkDispatch;
internal class Request
{
public string DefinitionId { get; set; } = default!;
public VersionOptions? VersionOptions { get; set; }
public string? TriggerActivityId { get; set; }
[JsonConverter(typeof(ExpandoObjectConverterFactory))]
public object? Input { get; set; }
public int Count { get; set; } = 1;
}
internal record Response(ICollection<string> WorkflowInstanceIds);
@@ -0,0 +1,75 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkPublish;
[PublicAPI]
internal class BulkPublish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
public override void Configure()
{
Post("/bulk-actions/publish/workflow-definitions/by-definition-ids");
ConfigurePermissions("publish:workflow-definitions");
}
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.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 updatedConsumers = new List<string>();
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))
{
notFound.Add(definitionId);
continue;
}
if (definition.IsPublished)
{
alreadyPublished.Add(definitionId);
continue;
}
if (definition.IsReadonly)
{
skipped.Add(definitionId);
continue;
}
var result = await workflowDefinitionPublisher.PublishAsync(definition, cancellationToken);
published.Add(definitionId);
if (result.AffectedWorkflows.WorkflowDefinitions.Count > 0)
updatedConsumers.AddRange(result.AffectedWorkflows.WorkflowDefinitions.Select(x => x.DefinitionId));
}
return new(published, alreadyPublished, notFound, skipped, updatedConsumers);
}
}
@@ -0,0 +1,15 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkPublish;
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)
{
public ICollection<string> Published { get; } = published;
public ICollection<string> AlreadyPublished { get; } = alreadyPublished;
public ICollection<string> NotFound { get; } = notFound;
public ICollection<string> Skipped { get; } = skipped;
public ICollection<string> UpdatedConsumers { get; } = updatedConsumers;
}
@@ -0,0 +1,70 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkRetract;
[PublicAPI]
internal class BulkRetract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
public override void Configure()
{
Post("/bulk-actions/retract/workflow-definitions/by-definition-ids");
ConfigurePermissions("retract:workflow-definitions");
}
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.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
{
DefinitionId = definitionId,
VersionOptions = VersionOptions.LatestOrPublished
}, cancellationToken: cancellationToken)).ToList();
if (!definitions.Any())
{
notFound.Add(definitionId);
continue;
}
var published = definitions.FirstOrDefault(d => d.IsPublished);
if (published is null)
{
notPublished.Add(definitionId);
continue;
}
if (published.IsReadonly)
{
skipped.Add(definitionId);
continue;
}
await workflowDefinitionPublisher.RetractAsync(published, cancellationToken);
retracted.Add(definitionId);
}
return new Response(retracted, notPublished, notFound, skipped);
}
}
@@ -0,0 +1,18 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.BulkRetract;
internal class Request
{
public ICollection<string> DefinitionIds { get; set; } = default!;
}
internal class Response(ICollection<string> retracted, ICollection<string> alreadyRetracted, ICollection<string> notFound, ICollection<string> skipped)
{
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; } = alreadyRetracted;
public ICollection<string> NotFound { get; } = notFound;
}
@@ -0,0 +1,25 @@
using Elsa.Abstractions;
using Elsa.Workflows.Management;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Count;
/// <summary>
/// An endpoint for counting workflow definitions.
/// </summary>
[PublicAPI]
internal class Count(IWorkflowDefinitionStore store) : ElsaEndpointWithoutRequest<Response>
{
public override void Configure()
{
Get("/workflow-definitions/query/count");
ConfigurePermissions("read:workflow-definitions");
}
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var count = await store.CountDistinctAsync(cancellationToken);
var response = new Response(count);
await SendOkAsync(response, cancellationToken);
}
}
@@ -0,0 +1,3 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Count;
internal record Response(long Count);
@@ -0,0 +1,51 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Delete;
internal class Delete(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService, IWorkflowDefinitionStore store)
: ElsaEndpoint<Request>
{
public override void Configure()
{
Delete("/workflow-definitions/{definitionId}");
ConfigurePermissions("delete:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken 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 = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var result = await workflowDefinitionManager.DeleteByDefinitionIdAsync(request.DefinitionId, cancellationToken);
if (result == 0)
await SendNotFoundAsync(cancellationToken);
else
await SendNoContentAsync(cancellationToken);
}
}
@@ -0,0 +1,6 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Delete;
internal class Request
{
public string DefinitionId { get; set; } = default!;
}
@@ -0,0 +1,37 @@
using Elsa.Abstractions;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.DeleteVersion;
[PublicAPI]
internal class DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService)
: ElsaEndpoint<Request>
{
public override void Configure()
{
Delete("/workflow-definition-versions/{id}");
ConfigurePermissions("delete:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var deleted = await workflowDefinitionManager.DeleteByIdAsync(request.Id, cancellationToken);
if (!deleted)
await SendNotFoundAsync(cancellationToken);
else
await SendNoContentAsync(cancellationToken);
}
}
@@ -0,0 +1,6 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.DeleteVersion;
internal class Request
{
public string Id { get; set; } = default!;
}
@@ -0,0 +1,66 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Management;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Requests;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Dispatch;
[UsedImplicitly]
internal class Endpoint(IWorkflowDefinitionService workflowDefinitionService, IWorkflowDispatcher workflowDispatcher, IIdentityGenerator identityGenerator) : ElsaEndpoint<Request, Response>
{
public override void Configure()
{
Post("/workflow-definitions/{definitionId}/dispatch");
ConfigurePermissions("exec:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var definitionId = request.DefinitionId;
var versionOptions = request.VersionOptions ?? VersionOptions.Published;
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, versionOptions, cancellationToken);
if(workflowGraph == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var input = request.Input;
if(input != null && input is not IDictionary<string, object>)
{
AddError("Input must be a dictionary.");
await SendErrorsAsync(cancellation: cancellationToken);
return;
}
var instanceId = request.InstanceId ?? identityGenerator.GenerateId();
var dispatchRequest = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id)
{
Input = input as IDictionary<string, object>,
InstanceId = instanceId,
CorrelationId = request.CorrelationId,
TriggerActivityId = request.TriggerActivityId,
};
var options = new DispatchWorkflowOptions
{
Channel = request.Channel
};
var result = await workflowDispatcher.DispatchAsync(dispatchRequest, options, cancellationToken);
if(!result.Succeeded)
{
var fault = result.Fault!;
AddError(fault.Message, fault.Code);
await SendErrorsAsync(cancellation: cancellationToken);
return;
}
await SendOkAsync(new Response(instanceId), cancellationToken);
}
}
@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
using Elsa.Common.Models;
using Elsa.Workflows.Serialization.Converters;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Dispatch;
internal class Request
{
public string DefinitionId { get; set; } = default!;
public string? InstanceId { get; set; }
public string? CorrelationId { get; set; }
public string? TriggerActivityId { get; set; }
public VersionOptions? VersionOptions { get; set; }
[JsonConverter(typeof(ExpandoObjectConverterFactory))]
public object? Input { get; set; }
public string? Channel { get; set; }
}
internal record Response(string WorkflowInstanceId);
@@ -0,0 +1,106 @@
using System.Net.Mime;
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Management;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.State;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Execute;
/// <summary>
/// This abstract class provides the necessary infrastructure to handle the execution of workflows, including setup of routes, permissions,
/// and processing of HTTP requests to execute workflows.
/// </summary>
internal abstract class EndpointBase<T>(
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowRuntime workflowRuntime,
IWorkflowStarter workflowStarter,
IApiSerializer apiSerializer)
: ElsaEndpoint<T, Response> where T : IExecutionRequest, new()
{
/// <inheritdoc />
public override void Configure()
{
Routes("/workflow-definitions/{definitionId}/execute");
ConfigurePermissions("exec:workflow-definitions");
}
/// <inheritdoc />
public override async Task HandleAsync(T request, CancellationToken cancellationToken)
{
var definitionId = request.DefinitionId;
var versionOptions = request.VersionOptions ?? VersionOptions.Published;
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(definitionId, versionOptions, cancellationToken);
if (workflowGraph == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var startRequest = new StartWorkflowRequest
{
Workflow = workflowGraph.Workflow,
CorrelationId = request.CorrelationId,
Name = request.Name,
Input = request.GetInputAsDictionary(),
Variables = request.GetVariablesAsDictionary(),
TriggerActivityId = request.TriggerActivityId,
ActivityHandle = request.ActivityHandle
};
var startResponse = await workflowStarter.StartWorkflowAsync(startRequest, cancellationToken);
if(!HttpContext.Response.HasStarted)
HttpContext.Response.Headers.Append("x-elsa-workflow-cannot-start", startResponse.CannotStart.ToString());
if (startResponse.CannotStart)
{
await SendOkAsync(cancellationToken);
return;
}
var instanceId = startResponse.WorkflowInstanceId!;
// Write the workflow instance ID to the response header.
// This allows clients to read the header even if the workflow writes a response body
// (in which case, we can't transmit a JSON body that includes the instance ID).
if(!HttpContext.Response.HasStarted)
HttpContext.Response.Headers.Append("x-elsa-workflow-instance-id", instanceId);
var workflowClient = await workflowRuntime.CreateClientAsync(instanceId, cancellationToken);
// If a workflow fault occurred, respond appropriately with a 500 internal server error.
if (startResponse.SubStatus == WorkflowSubStatus.Faulted)
{
var workflowState = await workflowClient.ExportStateAsync(cancellationToken);
await HandleFaultAsync(workflowState, cancellationToken);
}
else
{
if (!HttpContext.Response.HasStarted)
{
// Write a response header to indicate that the response is a workflow state response.
// This is used by tools like Elsa Studio to determine if the response is in response to a workflow execution manually triggered by the user.
HttpContext.Response.Headers.Append("x-elsa-response", "true");
// Only write a response if the workflow didn't change the HTTP status code.
if (HttpContext.Response.StatusCode == StatusCodes.Status200OK)
{
var workflowState = await workflowClient.ExportStateAsync(cancellationToken);
await SendOkAsync(new(workflowState), cancellationToken);
}
}
}
}
private async Task HandleFaultAsync(WorkflowState workflowState, CancellationToken cancellationToken)
{
var faultedResponse = apiSerializer.Serialize(new Response(workflowState));
HttpContext.Response.ContentType = MediaTypeNames.Application.Json;
HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
await HttpContext.Response.WriteAsync(faultedResponse, cancellationToken);
}
}
@@ -0,0 +1,24 @@
using Elsa.Workflows.Management;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Execute;
/// <summary>
/// An API endpoint that executes a given workflow definition through GET method.
/// </summary>
[PublicAPI]
internal class GetEndpoint(
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowRuntime workflowRuntime,
IWorkflowStarter workflowStarter,
IApiSerializer apiSerializer)
: EndpointBase<GetRequest>(workflowDefinitionService, workflowRuntime, workflowStarter, apiSerializer)
{
/// <inheritdoc />
public override void Configure()
{
base.Configure();
Verbs(FastEndpoints.Http.GET);
}
}
@@ -0,0 +1,75 @@
using System.Dynamic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Common.Models;
using Elsa.Expressions.Helpers;
using Elsa.Workflows.Models;
using Elsa.Workflows.Serialization.Converters;
using Elsa.Workflows.State;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Execute;
public interface IExecutionRequest
{
string DefinitionId { get; }
string? CorrelationId { get; }
string? Name { get; }
string? TriggerActivityId { get; }
ActivityHandle? ActivityHandle { get; }
VersionOptions? VersionOptions { get; }
IDictionary<string, object>? GetInputAsDictionary();
IDictionary<string, object>? GetVariablesAsDictionary();
}
public class PostRequest : IExecutionRequest
{
public string DefinitionId { get; set; } = null!;
public string? CorrelationId { get; set; }
public string? Name { get; set; }
public string? TriggerActivityId { get; set; }
public ActivityHandle? ActivityHandle { get; set; }
public VersionOptions? VersionOptions { get; set; }
[JsonConverter(typeof(ExpandoObjectConverterFactory))]
public object? Input { get; set; }
[JsonConverter(typeof(ExpandoObjectConverterFactory))]
public object? Variables { get; set; }
public IDictionary<string, object>? GetInputAsDictionary() => (IDictionary<string, object>?)Input;
public IDictionary<string, object>? GetVariablesAsDictionary() => (IDictionary<string, object>?)Variables;
}
public class GetRequest : IExecutionRequest
{
public string DefinitionId { get; set; } = null!;
public string? CorrelationId { get; set; }
public string? Name { get; set; }
public string? TriggerActivityId { get; set; }
public ActivityHandle? ActivityHandle { get; set; }
public VersionOptions? VersionOptions { get; set; }
public string? Input { get; set; }
public string? Variables { get; set; }
public IDictionary<string, object>? GetInputAsDictionary() => ParseStringAsDictionary(Input);
public IDictionary<string, object>? GetVariablesAsDictionary() => ParseStringAsDictionary(Variables);
private IDictionary<string, object>? ParseStringAsDictionary(string? value)
{
var result = value?.TryConvertTo<ExpandoObject>(new()
{
SerializerOptions = new()
{
Converters = { new ExpandoObjectConverter() }
}
});
return result?.Success == true ? (IDictionary<string, object>?)result.Value : null;
}
}
public class Response(WorkflowState workflowState)
{
public WorkflowState WorkflowState { get; } = workflowState;
}
@@ -0,0 +1,24 @@
using Elsa.Workflows.Management;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Execute;
/// <summary>
/// An API endpoint that executes a given workflow definition through POST method.
/// </summary>
[PublicAPI]
internal class PostEndpoint(
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowRuntime workflowRuntime,
IWorkflowStarter workflowStarter,
IApiSerializer apiSerializer)
: EndpointBase<PostRequest>(workflowDefinitionService, workflowRuntime, workflowStarter, apiSerializer)
{
/// <inheritdoc />
public override void Configure()
{
base.Configure();
Verbs(FastEndpoints.Http.POST);
}
}
@@ -0,0 +1,144 @@
using System.IO.Compression;
using System.Text.Json;
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using Humanizer;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Export;
/// <summary>
/// Exports the specified workflow definition as JSON download.
/// </summary>
[UsedImplicitly]
internal class Export : ElsaEndpoint<Request>
{
private readonly IApiSerializer _serializer;
private readonly IWorkflowDefinitionStore _store;
private readonly WorkflowDefinitionMapper _workflowDefinitionMapper;
/// <inheritdoc />
public Export(
IWorkflowDefinitionStore store,
IWorkflowDefinitionService workflowDefinitionService,
IApiSerializer serializer,
WorkflowDefinitionMapper workflowDefinitionMapper,
VariableDefinitionMapper variableDefinitionMapper)
{
_store = store;
_serializer = serializer;
_workflowDefinitionMapper = workflowDefinitionMapper;
}
/// <inheritdoc />
public override void Configure()
{
Routes("/bulk-actions/export/workflow-definitions", "/workflow-definitions/{definitionId}/export");
Verbs(FastEndpoints.Http.GET, FastEndpoints.Http.POST);
ConfigurePermissions("read:workflow-definitions");
}
/// <inheritdoc />
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
if (request.DefinitionId != null)
await DownloadSingleWorkflowAsync(request.DefinitionId, request.VersionOptions, cancellationToken);
else
await DownloadMultipleWorkflowsAsync(request.Ids, cancellationToken);
}
private async Task DownloadMultipleWorkflowsAsync(ICollection<string> ids, CancellationToken cancellationToken)
{
List<WorkflowDefinition> definitions = (await _store.FindManyAsync(new WorkflowDefinitionFilter
{
Ids = ids
}, cancellationToken)).ToList();
if (!definitions.Any())
{
await SendNoContentAsync(cancellationToken);
return;
}
var zipStream = new MemoryStream();
using (var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
{
// Create a JSON file for each workflow definition:
foreach (var definition in definitions)
{
var model = await CreateWorkflowModelAsync(definition, cancellationToken);
var binaryJson = await SerializeWorkflowDefinitionAsync(model, cancellationToken);
var fileName = GetFileName(model);
var entry = zipArchive.CreateEntry(fileName, CompressionLevel.Optimal);
await using var entryStream = entry.Open();
await entryStream.WriteAsync(binaryJson, cancellationToken);
}
}
// Send the zip file to the client:
zipStream.Position = 0;
await SendBytesAsync(zipStream.ToArray(), "workflow-definitions.zip", cancellation: cancellationToken);
}
private async Task DownloadSingleWorkflowAsync(string definitionId, string? versionOptions, CancellationToken cancellationToken)
{
var parsedVersionOptions = string.IsNullOrEmpty(versionOptions) ? VersionOptions.Latest : VersionOptions.FromString(versionOptions);
WorkflowDefinition? definition = (await _store.FindManyAsync(new WorkflowDefinitionFilter
{
DefinitionId = definitionId,
VersionOptions = parsedVersionOptions
}, cancellationToken)).FirstOrDefault();
if (definition == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var model = await CreateWorkflowModelAsync(definition, cancellationToken);
var binaryJson = await SerializeWorkflowDefinitionAsync(model, cancellationToken);
var fileName = GetFileName(model);
await SendBytesAsync(binaryJson, fileName, cancellation: cancellationToken);
}
private string GetFileName(WorkflowDefinitionModel definition)
{
var hasWorkflowName = !string.IsNullOrWhiteSpace(definition.Name);
var workflowName = hasWorkflowName ? definition.Name!.Trim() : definition.DefinitionId;
var fileName = $"workflow-definition-{workflowName.Underscore().Dasherize().ToLowerInvariant()}.json";
return fileName;
}
private async Task<byte[]> SerializeWorkflowDefinitionAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken)
{
var serializerOptions = _serializer.GetOptions();
var document = JsonSerializer.SerializeToDocument(model, serializerOptions);
var rootElement = document.RootElement;
using var output = new MemoryStream();
await using var writer = new Utf8JsonWriter(output);
writer.WriteStartObject();
writer.WriteString("$schema", "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json");
foreach (var property in rootElement.EnumerateObject())
property.WriteTo(writer);
writer.WriteEndObject();
await writer.FlushAsync(cancellationToken);
var binaryJson = output.ToArray();
return binaryJson;
}
private async Task<WorkflowDefinitionModel> CreateWorkflowModelAsync(WorkflowDefinition definition, CancellationToken cancellationToken)
{
return await _workflowDefinitionMapper.MapAsync(definition, cancellationToken);
}
}
@@ -0,0 +1,23 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Export;
/// <summary>
/// Exports the specified workflow definition as JSON download.
/// Either specify the <see cref="DefinitionId"/> and optionally the <see cref="VersionOptions"/> or the <see cref="Ids"/> of the workflow definition versions to export.
/// </summary>
internal class Request
{
/// <summary>
/// The workflow definition ID.
/// </summary>
public string? DefinitionId { get; set; } = default!;
/// <summary>
/// The version options.
/// </summary>
public string? VersionOptions { get; set; }
/// <summary>
/// A list of workflow definition version IDs.
/// </summary>
public ICollection<string>? Ids { get; set; } = default!;
}
@@ -0,0 +1,33 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Management;
using Elsa.Workflows.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetByDefinitionId;
[PublicAPI]
internal class GetByDefinitionId(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker linker) : ElsaEndpoint<Request>
{
public override void Configure()
{
Get("/workflow-definitions/by-definition-id/{definitionId}", "/workflow-definitions/{definitionId}");
ConfigurePermissions("read:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var versionOptions = request.VersionOptions != null ? VersionOptions.FromString(request.VersionOptions) : VersionOptions.Latest;
var filter = WorkflowDefinitionHandle.ByDefinitionId(request.DefinitionId, versionOptions).ToFilter();
var definition = await store.FindAsync(filter, cancellationToken);
if (definition == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var model = await linker.MapAsync(definition, cancellationToken);
await SendOkAsync(model, cancellationToken);
}
}
@@ -0,0 +1,7 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetByDefinitionId;
internal class Request
{
public string DefinitionId { get; set; } = default!;
public string? VersionOptions { get; set; }
}
@@ -0,0 +1,37 @@
using Elsa.Abstractions;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Builder;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetById;
[PublicAPI]
internal class GetById(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker linker) : ElsaEndpoint<Request>
{
public override void Configure()
{
Get("/workflow-definitions/by-id/{id}");
ConfigurePermissions("read:workflow-definitions");
Options(x => x.WithName("GetWorkflowDefinitionById"));
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var filter = new WorkflowDefinitionFilter
{
Id = request.Id
};
var definition = await store.FindAsync(filter, cancellationToken);
if (definition == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var model = await linker.MapAsync(definition, cancellationToken);
await SendOkAsync(model, cancellationToken);
}
}
@@ -0,0 +1,6 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetById;
internal class Request
{
public string Id { get; set; } = default!;
}
@@ -0,0 +1,31 @@
using Elsa.Abstractions;
using Elsa.Models;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetManyById;
[PublicAPI]
internal class GetManyById(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker linker) : ElsaEndpoint<Request>
{
public override void Configure()
{
Get("/workflow-definitions/many-by-id");
ConfigurePermissions("read:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var filter = new WorkflowDefinitionFilter
{
Ids = request.Ids
};
var definitions = (await store.FindManyAsync(filter, cancellationToken)).ToList();
var models = await linker.MapAsync(definitions, cancellationToken);
var response = new ListResponse<LinkedWorkflowDefinitionModel>(models);
await SendOkAsync(response, cancellationToken);
}
}
@@ -0,0 +1,6 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetManyById;
internal class Request
{
public ICollection<string> Ids { get; set; } = default!;
}
@@ -0,0 +1,35 @@
using Elsa.Abstractions;
using Elsa.Extensions;
using Elsa.Workflows.Management;
using Elsa.Workflows.Serialization.Converters;
using Elsa.Workflows.Serialization.Helpers;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Graph;
[PublicAPI]
internal class Graph(IWorkflowDefinitionService workflowDefinitionService, IApiSerializer apiSerializer, ActivityWriter activityWriter) : ElsaEndpoint<Request>
{
public override void Configure()
{
Get("/workflow-definitions/subgraph/{id}");
ConfigurePermissions("read:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(request.Id, cancellationToken);
if (workflowGraph == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var parentNode = workflowGraph.NodeIdLookup.TryGetValue(request.ParentNodeId, out var node) ? node : workflowGraph.Root;
var serializerOptions = apiSerializer.GetOptions().Clone();
serializerOptions.Converters.Add(new RootActivityNodeConverter(activityWriter));
await HttpContext.Response.WriteAsJsonAsync(parentNode, serializerOptions, cancellationToken);
}
}
@@ -0,0 +1,14 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Graph;
internal class Request
{
/// <summary>
/// The workflow definition version ID.
/// </summary>
public string Id { get; set; } = default!;
/// <summary>
/// The ID of the node to select the subgraph for.
/// </summary>
public string ParentNodeId { get; set; } = default!;
}
@@ -0,0 +1,68 @@
using Elsa.Abstractions;
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Management;
using Elsa.Workflows.Serialization.Converters;
using Elsa.Workflows.Serialization.Helpers;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Graph.Segments;
[PublicAPI]
internal class Nodes(IWorkflowDefinitionService workflowDefinitionService, IApiSerializer apiSerializer, ActivityWriter activityWriter) : ElsaEndpoint<Request>
{
public override void Configure()
{
Get("/workflow-definitions/subgraph/segments/{id}");
ConfigurePermissions("read:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(request.Id, cancellationToken);
if (workflowGraph?.Root == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var nodeId = request.ChildNodeId;
if (!workflowGraph.NodeIdLookup.TryGetValue(nodeId, out var childNode))
{
AddError("Unknown node ID");
await SendErrorsAsync(cancellation: cancellationToken);
return;
}
var ancestors = childNode.Ancestors().ToList();
var serializerOptions = apiSerializer.GetOptions().Clone().WithConverters(new RootActivityNodeConverter(activityWriter));
var segments = new Stack<ActivityPathSegment>();
var currentNode = ancestors.FirstOrDefault();
var previousNode = childNode;
while (currentNode != null)
{
if (currentNode.Activity is not Workflow and not Flowchart)
{
var currentActivity = currentNode.Activity;
var currentPort = previousNode.Port;
var activityName = currentActivity.Name ?? currentActivity.Type;
var segment = new ActivityPathSegment(currentNode.NodeId, currentActivity.Id, currentActivity.Type, currentPort, activityName);
segments.Push(segment);
}
previousNode = currentNode;
currentNode = currentNode.Parents.FirstOrDefault();
}
var leafSegment = segments.LastOrDefault();
var container = leafSegment == null ? ancestors.LastOrDefault() ?? childNode : ancestors.Last(x => x.Activity.Id == leafSegment.ActivityId);
var response = new Response(childNode, container, segments.ToList());
await HttpContext.Response.WriteAsJsonAsync(response, serializerOptions, cancellationToken);
}
}
@@ -0,0 +1,19 @@
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Models;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Graph.Segments;
internal class Request
{
/// <summary>
/// The workflow definition version ID.
/// </summary>
public string Id { get; set; } = default!;
/// <summary>
/// The ID of the child node to get segments for.
/// </summary>
public string ChildNodeId { get; set; } = default!;
}
internal record Response(ActivityNode ChildNode, ActivityNode Container, ICollection<ActivityPathSegment> PathSegments);
@@ -0,0 +1,89 @@
using Elsa.Abstractions;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Import;
/// <summary>
/// Imports JSON and/or ZIP files containing a workflow definitions.
/// </summary>
[PublicAPI]
internal class Import : ElsaEndpoint<WorkflowDefinitionModel>
{
private readonly IWorkflowDefinitionImporter _workflowDefinitionImporter;
private readonly IWorkflowDefinitionLinker _linker;
private readonly IAuthorizationService _authorizationService;
/// <inheritdoc />
public Import(
IWorkflowDefinitionImporter workflowDefinitionImporter,
IWorkflowDefinitionLinker linker,
IAuthorizationService authorizationService)
{
_workflowDefinitionImporter = workflowDefinitionImporter;
_linker = linker;
_authorizationService = authorizationService;
}
/// <inheritdoc />
public override void Configure()
{
Routes("workflow-definitions/import", "workflow-definitions/{definitionId}/import");
Verbs(FastEndpoints.Http.POST, FastEndpoints.Http.PUT);
ConfigurePermissions("write:workflow-definitions");
}
/// <inheritdoc />
public override async Task HandleAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken)
{
var definitionId = model.DefinitionId;
var isNew = string.IsNullOrWhiteSpace(definitionId);
var result = await ImportSingleWorkflowDefinitionAsync(model, cancellationToken);
var definition = result.WorkflowDefinition;
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var updatedModel = await _linker.MapAsync(definition, cancellationToken);
if (result.Succeeded)
{
if (isNew)
await SendCreatedAtAsync<GetByDefinitionId.GetByDefinitionId>(new { DefinitionId = definitionId }, updatedModel, cancellation: cancellationToken);
else
await SendOkAsync(updatedModel, cancellationToken);
}
if (ValidationFailed)
await SendErrorsAsync(400, cancellationToken);
}
private async Task<ImportWorkflowResult> ImportSingleWorkflowDefinitionAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken)
{
// Import workflow
var saveWorkflowRequest = new SaveWorkflowDefinitionRequest
{
Model = model,
Publish = false,
};
var result = await _workflowDefinitionImporter.ImportAsync(saveWorkflowRequest, cancellationToken);
if (result.Succeeded)
return result;
foreach (var validationError in result.ValidationErrors)
AddError(validationError.Message);
return result;
}
}
@@ -0,0 +1,135 @@
using System.IO.Compression;
using Elsa.Abstractions;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
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;
/// <summary>
/// Imports JSON and/or ZIP files containing a workflow definitions.
/// </summary>
[PublicAPI]
internal class ImportFiles : ElsaEndpoint<WorkflowDefinitionModel>
{
private readonly IWorkflowDefinitionService _workflowDefinitionService;
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,
IAuthorizationService authorizationService)
{
_workflowDefinitionService = workflowDefinitionService;
_workflowDefinitionImporter = workflowDefinitionImporter;
_workflowDefinitionMapper = workflowDefinitionMapper;
_apiSerializer = apiSerializer;
_authorizationService = authorizationService;
}
/// <inheritdoc />
public override void Configure()
{
Post("workflow-definitions/import-files");
ConfigurePermissions("write:workflow-definitions");
AllowFileUploads();
}
/// <inheritdoc />
public override async Task HandleAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken)
{
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.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);
}
private async Task<int> ImportFilesAsync(IFormFileCollection files, CancellationToken cancellationToken)
{
var count = 0;
foreach (var file in files)
{
var fileStream = file.OpenReadStream();
// Check if the file is a JSON file or a ZIP file.
var isJsonFile = file.ContentType == "application/json";
// If the file is a JSON file, read it.
if (isJsonFile)
{
await ImportJsonStreamAsync(fileStream, cancellationToken);
count++;
}
else
{
// If the file is a ZIP file, extract the JSON files and read them.
var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read);
foreach (var entry in zipArchive.Entries)
{
if (!entry.FullName.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
continue;
var jsonStream = entry.Open();
await ImportJsonStreamAsync(jsonStream, cancellationToken);
count++;
}
}
}
return count;
}
private async Task ImportJsonStreamAsync(Stream jsonStream, CancellationToken cancellationToken)
{
var json = await new StreamReader(jsonStream).ReadToEndAsync(cancellationToken);
var model = _apiSerializer.Deserialize<WorkflowDefinitionModel>(json);
await ImportSingleWorkflowDefinitionAsync(model, cancellationToken);
}
private async Task<ImportWorkflowResult> ImportSingleWorkflowDefinitionAsync(WorkflowDefinitionModel model, CancellationToken cancellationToken)
{
// Import workflow
var saveWorkflowRequest = new SaveWorkflowDefinitionRequest
{
Model = model,
Publish = false,
};
var result = await _workflowDefinitionImporter.ImportAsync(saveWorkflowRequest, cancellationToken);
if (result.Succeeded)
return result;
foreach (var validationError in result.ValidationErrors)
AddError(validationError.Message);
return result;
}
}
@@ -0,0 +1,26 @@
using Elsa.Abstractions;
using Elsa.Workflows.Management;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.IsNameUnique;
/// <summary>
/// Checks if a workflow definition name is unique.
/// </summary>
[PublicAPI]
internal class IsNameUnique(IWorkflowDefinitionStore store) : ElsaEndpoint<Request>
{
public override void Configure()
{
Get("/workflow-definitions/validation/is-name-unique");
ConfigurePermissions("read:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var isUnique = await store.GetIsNameUnique(request.Name.Trim(), request.DefinitionId, cancellationToken);
var response = new Response(isUnique);
await SendOkAsync(response, cancellationToken);
}
}
@@ -0,0 +1,20 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.IsNameUnique;
internal class Request
{
/// <summary>
/// The name to check.
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// Optional. If provided, the name will be checked against all workflow definitions except the one with the specified ID.
/// </summary>
public string? DefinitionId { get; set; }
}
/// <summary>
/// Represents a response indicating whether a workflow definition name is unique.
/// </summary>
/// <param name="IsUnique">A value indicating whether the name is unique.</param>
internal record Response(bool IsUnique);
@@ -0,0 +1,87 @@
using Elsa.Abstractions;
using Elsa.Common.Entities;
using Elsa.Common.Models;
using Elsa.Models;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.List;
[PublicAPI]
internal class List(IWorkflowDefinitionStore store, IWorkflowDefinitionLinker linker) : ElsaEndpoint<Request, PagedListResponse<LinkedWorkflowDefinitionSummary>>
{
public override void Configure()
{
Get("/workflow-definitions");
ConfigurePermissions("read:workflow-definitions");
}
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);
var pagedList = new PagedListResponse<WorkflowDefinitionSummary>(summaries);
var response = linker.MapAsync(pagedList);
return response;
}
private WorkflowDefinitionFilter CreateFilter(Request request)
{
var versionOptions = string.IsNullOrWhiteSpace(request.VersionOptions) ? default(VersionOptions?) : VersionOptions.FromString(request.VersionOptions);
return new()
{
IsSystem = request.IsSystem,
VersionOptions = versionOptions,
SearchTerm = request.SearchTerm?.Trim(),
MaterializerName = request.MaterializerName,
DefinitionIds = request.DefinitionIds,
Ids = request.Ids
};
}
private async Task<Page<WorkflowDefinitionSummary>> FindAsync(Request request, WorkflowDefinitionFilter filter, PageArgs pageArgs, CancellationToken cancellationToken)
{
request.OrderBy ??= OrderByWorkflowDefinition.Name;
var direction = request.OrderBy == OrderByWorkflowDefinition.Name ? request.OrderDirection ?? OrderDirection.Ascending : request.OrderDirection ?? OrderDirection.Descending;
switch (request.OrderBy)
{
default:
{
var order = new WorkflowDefinitionOrder<DateTimeOffset>
{
KeySelector = p => p.CreatedAt,
Direction = direction
};
return await store.FindSummariesAsync(filter, order, pageArgs, cancellationToken);
}
case OrderByWorkflowDefinition.Name:
{
var order = new WorkflowDefinitionOrder<string>
{
KeySelector = p => p.Name!,
Direction = direction
};
return await store.FindSummariesAsync(filter, order, pageArgs, cancellationToken);
}
case OrderByWorkflowDefinition.Version:
{
var order = new WorkflowDefinitionOrder<int>
{
KeySelector = p => p.Version,
Direction = direction
};
return await store.FindSummariesAsync(filter, order, pageArgs, cancellationToken);
}
}
}
}
@@ -0,0 +1,20 @@
using Elsa.Common.Entities;
using Elsa.Workflows.Api.Models;
using FastEndpoints;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.List;
internal class Request
{
public string? VersionOptions { get; set; }
public string[]? DefinitionIds { get; set; }
public string[]? Ids { get; set; }
[BindFrom("materializer")] public string? MaterializerName { get; set; }
[BindFrom("label")] public string[]? Labels { get; set; }
public int? Page { get; set; }
public int? PageSize { get; set; }
public OrderByWorkflowDefinition? OrderBy { get; set; }
public OrderDirection? OrderDirection { get; set; }
public string? SearchTerm { get; set; }
public bool? IsSystem { get; set; }
}
@@ -0,0 +1,113 @@
using System.Text.Json;
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Materializers;
using Elsa.Workflows.Management.Models;
using Elsa.Workflows.Models;
using JetBrains.Annotations;
using Medallion.Threading;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Post;
[PublicAPI]
internal class Post(
IApiSerializer serializer,
IWorkflowDefinitionPublisher workflowDefinitionPublisher,
VariableDefinitionMapper variableDefinitionMapper,
IDistributedLockProvider distributedLockProvider,
IWorkflowDefinitionLinker linker,
IAuthorizationService authorizationService)
: ElsaEndpoint<SaveWorkflowDefinitionRequest, LinkedWorkflowDefinitionModel>
{
public override void Configure()
{
Post("/workflow-definitions");
ConfigurePermissions("write:workflow-definitions");
}
public override async Task HandleAsync(SaveWorkflowDefinitionRequest request, CancellationToken cancellationToken)
{
var model = request.Model;
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);
var draft = !string.IsNullOrWhiteSpace(definitionId)
? await workflowDefinitionPublisher.GetDraftAsync(definitionId, VersionOptions.Latest, cancellationToken)
: null;
var isNew = draft == null;
// Create a new workflow in case no existing definition was found.
if (isNew)
{
draft = await workflowDefinitionPublisher.NewAsync(cancellationToken: cancellationToken);
if (!string.IsNullOrWhiteSpace(definitionId))
draft.DefinitionId = definitionId;
}
var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(draft), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
// Update the draft with the received model.
var root = model.Root ?? new Sequence();
var serializerOptions = serializer.GetOptions();
var stringData = JsonSerializer.Serialize(root, serializerOptions);
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>();
draft!.StringData = stringData;
draft.MaterializerName = JsonWorkflowMaterializer.MaterializerName;
draft.Name = model.Name?.Trim();
draft.ToolVersion = model.ToolVersion;
draft.Description = model.Description?.Trim();
draft.CustomProperties = model.CustomProperties ?? new Dictionary<string, object>();
draft.Variables = variables;
draft.Inputs = inputs;
draft.Outputs = outputs;
draft.Outcomes = outcomes;
draft.Options = model.Options ?? new WorkflowOptions();
PublishWorkflowDefinitionResult? result = null;
if (request.Publish == true)
{
result = await workflowDefinitionPublisher.PublishAsync(draft, cancellationToken);
if (!result.Succeeded)
{
foreach (var validationError in result.ValidationErrors)
AddError(validationError.Message);
await SendErrorsAsync(400, cancellationToken);
return;
}
}
else
{
await workflowDefinitionPublisher.SaveDraftAsync(draft, cancellationToken);
}
var mappedDefinition = await linker.MapAsync(draft, cancellationToken);
var affectedWorkflows = result?.AffectedWorkflows?.WorkflowDefinitions ?? [];
var response = new Response(mappedDefinition, false, affectedWorkflows.Count);
await HttpContext.Response.WriteAsJsonAsync(response, serializerOptions, cancellationToken);
}
}
@@ -0,0 +1,5 @@
using Elsa.Workflows.Api.Models;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Post;
internal record Response(LinkedWorkflowDefinitionModel WorkflowDefinition, bool AlreadyPublished, int ConsumingWorkflowCount);
@@ -0,0 +1,52 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Publish;
[PublicAPI]
internal class Publish(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IWorkflowDefinitionLinker linker, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
public override void Configure()
{
Post("/workflow-definitions/{definitionId}/publish");
ConfigurePermissions("publish:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken 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 = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var isPublished = definition.IsPublished;
var result = !isPublished ? await workflowDefinitionPublisher.PublishAsync(definition, cancellationToken) : null;
var mappedDefinition = await linker.MapAsync(definition, cancellationToken);
var response = new Response(mappedDefinition, isPublished, result?.AffectedWorkflows.WorkflowDefinitions.Count ?? 0);
await SendOkAsync(response, cancellationToken);
}
}
@@ -0,0 +1,10 @@
using Elsa.Workflows.Api.Models;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Publish;
internal class Request
{
public string DefinitionId { get; set; } = default!;
}
internal record Response(LinkedWorkflowDefinitionModel WorkflowDefinition, bool AlreadyPublished, int ConsumingWorkflowCount);
@@ -0,0 +1,31 @@
using Elsa.Abstractions;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Requests;
using Elsa.Workflows.Runtime.Responses;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Refresh;
[PublicAPI]
internal class Refresh(IWorkflowDefinitionsRefresher workflowDefinitionsRefresher) : ElsaEndpoint<Request>
{
private const int BatchSize = 10;
public override void Configure()
{
Post("/actions/workflow-definitions/refresh");
ConfigurePermissions("actions:workflow-definitions:refresh");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var result = await RefreshWorkflowDefinitionsAsync(request.DefinitionIds, cancellationToken);
await SendOkAsync(new Response(result.Refreshed, result.NotFound), cancellationToken);
}
private async Task<RefreshWorkflowDefinitionsResponse> RefreshWorkflowDefinitionsAsync(ICollection<string>? definitionIds, CancellationToken cancellationToken)
{
var request = new RefreshWorkflowDefinitionsRequest(definitionIds, BatchSize);
return await workflowDefinitionsRefresher.RefreshWorkflowDefinitionsAsync(request, cancellationToken);
}
}
@@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Refresh;
internal class Request
{
public ICollection<string>? DefinitionIds { get; set; }
}
internal class Response(ICollection<string> refreshed, ICollection<string> notFound)
{
[JsonPropertyName("refreshed")] public ICollection<string> Refreshed { get; } = refreshed;
[JsonPropertyName("notFound")] public ICollection<string> NotFound { get; } = notFound;
}
@@ -0,0 +1,28 @@
using Elsa.Abstractions;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Reload;
[PublicAPI]
internal class Reload(IWorkflowDefinitionsReloader workflowDefinitionsReloader) : ElsaEndpointWithoutRequest
{
private const int BatchSize = 10;
public override void Configure()
{
Post("/actions/workflow-definitions/reload");
ConfigurePermissions("actions:workflow-definitions:reload");
}
public override async Task HandleAsync(CancellationToken cancellationToken)
{
await ReloadWorkflowDefinitionsAsync(cancellationToken);
await SendOkAsync(cancellationToken);
}
private async Task ReloadWorkflowDefinitionsAsync(CancellationToken cancellationToken)
{
await workflowDefinitionsReloader.ReloadWorkflowDefinitionsAsync(cancellationToken);
}
}
@@ -0,0 +1,58 @@
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;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Retract;
[PublicAPI]
internal class Retract(IWorkflowDefinitionStore store, IWorkflowDefinitionPublisher workflowDefinitionPublisher, IWorkflowDefinitionLinker linker, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, LinkedWorkflowDefinitionModel>
{
public override void Configure()
{
Post("/workflow-definitions/{definitionId}/retract");
ConfigurePermissions("retract:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var filter = new WorkflowDefinitionFilter
{
DefinitionId = request.DefinitionId,
VersionOptions = VersionOptions.LatestOrPublished
};
var definition = await store.FindAsync(filter, cancellationToken);
if (definition == null)
{
await SendNotFoundAsync(cancellationToken);
return;
}
var authorizationResult = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
if (!definition.IsPublished)
{
AddError($"Workflow with id {request.DefinitionId} is not published");
await SendErrorsAsync(cancellation: cancellationToken);
return;
}
await workflowDefinitionPublisher.RetractAsync(definition, cancellationToken);
var response = await linker.MapAsync(definition, cancellationToken);
await SendOkAsync(response, cancellationToken);
}
}
@@ -0,0 +1,6 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Retract;
public class Request
{
public string DefinitionId { get; set; } = default!;
}
@@ -0,0 +1,52 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
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(IWorkflowReferenceUpdater workflowReferenceUpdater, IWorkflowDefinitionStore store, IAuthorizationService authorizationService)
: ElsaEndpoint<Request, Response>
{
public override void Configure()
{
Post("/workflow-definitions/{definitionId}/update-references");
ConfigurePermissions("publish:workflow-definitions");
}
public override async Task HandleAsync(Request request, CancellationToken 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 = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var result = await workflowReferenceUpdater.UpdateWorkflowReferencesAsync(definition, cancellationToken);
var affectedWorkflows = result.UpdatedWorkflows;
var response = new Response(affectedWorkflows.Select(w => w.Name ?? w.DefinitionId));
await SendOkAsync(response, cancellationToken);
}
}
@@ -0,0 +1,8 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.UpdateReferences;
internal record Request
{
public string DefinitionId { get; set; } = default!;
}
internal record Response(IEnumerable<string> AffectedWorkflows);
@@ -0,0 +1,63 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
/// <summary>
/// Deletes a specific version of a workflow definition.
/// </summary>
[PublicAPI]
public class DeleteVersion(IWorkflowDefinitionManager workflowDefinitionManager, IWorkflowDefinitionStore store, IAuthorizationService authorizationService) : ElsaEndpointWithoutRequest
{
/// <inheritdoc />
public override void Configure()
{
Delete("workflow-definitions/{definitionId}/version/{version}");
ConfigurePermissions("delete:workflow-definitions");
}
/// <inheritdoc />
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var definitionId = Route<string>("definitionId")!;
var version = Route<int>("version");
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 = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var result = await workflowDefinitionManager.DeleteVersionAsync(definition, cancellationToken);
if (!result)
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendOkAsync(cancellationToken);
}
}
@@ -0,0 +1,41 @@
using Elsa.Abstractions;
using Elsa.Common.Entities;
using Elsa.Common.Models;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
[PublicAPI]
internal class ListVersions(IWorkflowDefinitionStore store) : ElsaEndpointWithoutRequest
{
public override void Configure()
{
Get("workflow-definitions/{definitionId}/versions");
ConfigurePermissions("read:workflow-definitions");
}
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var definitionId = Route<string>("definitionId")!;
var filter = new WorkflowDefinitionFilter
{
DefinitionId = definitionId,
VersionOptions = VersionOptions.All
};
var orderBy = new WorkflowDefinitionOrder<int>(x => x.Version, OrderDirection.Descending);
var definitions = await store.FindManyAsync(filter, orderBy, cancellationToken);
if (!definitions.Any())
{
await SendNotFoundAsync(cancellationToken);
return;
}
await SendAsync(definitions, StatusCodes.Status200OK, cancellationToken);
}
}
@@ -0,0 +1,58 @@
using Elsa.Abstractions;
using Elsa.Common.Models;
using Elsa.Workflows.Api.Constants;
using Elsa.Workflows.Api.Requirements;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
[PublicAPI]
internal class RevertVersion(IWorkflowDefinitionManager workflowDefinitionManager, IAuthorizationService authorizationService, IWorkflowDefinitionStore store)
: ElsaEndpointWithoutRequest
{
public override void Configure()
{
Post("workflow-definitions/{definitionId}/revert/{version}");
ConfigurePermissions("publish:workflow-definitions");
}
public override async Task HandleAsync(CancellationToken cancellationToken)
{
var definitionId = Route<string>("definitionId")!;
var version = Route<int>("version");
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 = await authorizationService.AuthorizeAsync(User, new NotReadOnlyResource(definition), AuthorizationPolicies.NotReadOnlyPolicy);
if (!authorizationResult.Succeeded)
{
await SendForbiddenAsync(cancellationToken);
return;
}
var newDefinition = await workflowDefinitionManager.RevertVersionAsync(definitionId, version, cancellationToken);
var newDefinitionSummary = WorkflowDefinitionSummary.FromDefinition(newDefinition);
await SendCreatedAtAsync("GetWorkflowDefinitionById", new
{
id = newDefinition.Id
}, newDefinitionSummary, cancellation: cancellationToken);
}
}
@@ -0,0 +1,36 @@
using Elsa.Abstractions;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.BulkCancel;
/// <summary>
/// Represents an endpoint for bulk cancelling workflow instances.
/// </summary>
[PublicAPI]
public class BulkCancel(IWorkflowCancellationService workflowCancellationService) : ElsaEndpoint<Request, Response>
{
/// <inheritdoc />
public override void Configure()
{
Post("/bulk-actions/cancel/workflow-instances/");
ConfigurePermissions("cancel:workflow-instances");
}
/// <inheritdoc />
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var tasks = new List<Task<int>>();
if (request.Ids is not null)
tasks.Add(workflowCancellationService.CancelWorkflowsAsync(request.Ids!, cancellationToken));
if (request.DefinitionVersionId is not null)
tasks.Add(workflowCancellationService.CancelWorkflowByDefinitionVersionAsync(request.DefinitionVersionId!, cancellationToken));
if (request.DefinitionId is not null)
tasks.Add(workflowCancellationService.CancelWorkflowByDefinitionAsync(request.DefinitionId!, request.VersionOptions!.Value, cancellationToken));
await Task.WhenAll(tasks);
return new(tasks.Sum(t => t.Result));
}
}
@@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
using Elsa.Common.Models;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.BulkCancel;
public class Request
{
/// <summary>
/// Gets or sets the IDs of the workflow instances to delete.
/// </summary>
public ICollection<string>? Ids { get; set; }
/// <summary>
/// Gets or sets the definition version id.
/// </summary>
public string? DefinitionVersionId { get; set; }
/// <summary>
/// Represents the ID of a workflow definition.
/// </summary>
/// <remarks>This should be used in combination with <see cref="VersionOptions"/></remarks>
public string? DefinitionId { get; set; }
/// <summary>
/// Represents the version options for getting a specific version.
/// </summary>
/// <remarks>This should be used in combination with <see cref="DefinitionId"/></remarks>
public VersionOptions? VersionOptions { get; set; }
}
public class Response(int cancelledCount)
{
[JsonPropertyName("cancelled")] public int CancelledCount { get; } = cancelledCount;
}
@@ -0,0 +1,25 @@
using Elsa.Common.Models;
using FastEndpoints;
using FluentValidation;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.BulkCancel;
/// <summary>
/// Validates the request for bulk canceling workflow instances.
/// </summary>
public class RequestValidator : Validator<Request>
{
/// <inheritdoc />
public RequestValidator()
{
RuleFor(x => x.VersionOptions)
.Must((request, options) => ValidCombination(request.DefinitionId, options))
.WithMessage("VersionOptions and DefinitionId should either both be empty or both be not empty");
}
private bool ValidCombination(string? definitionId, VersionOptions? options)
{
return (definitionId is null && options is null)
|| (definitionId is not null && options is not null);
}
}
@@ -0,0 +1,32 @@
using Elsa.Abstractions;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.BulkDelete;
[PublicAPI]
internal class BulkDelete(IWorkflowInstanceManager store) : ElsaEndpoint<Request, Response>
{
public override void Configure()
{
Post(
"/bulk-actions/delete/workflow-instances",
"/bulk-actions/delete/workflow-instances/by-id" // Deprecated route.
);
ConfigurePermissions("delete:workflow-instances");
}
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var filter = new WorkflowInstanceFilter
{
Ids = request.Ids,
DefinitionId = request.WorkflowDefinitionId,
DefinitionIds = request.WorkflowDefinitionIds,
};
var count = await store.BulkDeleteAsync(filter, cancellationToken);
return new(count);
}
}
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.BulkDelete;
internal class Request
{
public ICollection<string>? Ids { get; set; }
public string? WorkflowDefinitionId { get; set; }
public ICollection<string>? WorkflowDefinitionIds { get; set; }
}
internal class Response(long deletedCount)
{
[JsonPropertyName("deleted")] public long DeletedCount { get; } = deletedCount;
}
@@ -0,0 +1,26 @@
using Elsa.Abstractions;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Cancel;
[PublicAPI]
internal class Cancel(IWorkflowCancellationService workflowCancellationService)
: ElsaEndpoint<Request>
{
public override void Configure()
{
Post("/cancel/workflow-instances/{id}");
ConfigurePermissions("cancel:workflow-instances");
//Allows for post with empty body
Description(x => x.Accepts<Request>("*/*"), clearDefaults: true);
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
await workflowCancellationService.CancelWorkflowAsync(request.Id, cancellationToken);
await SendOkAsync(cancellationToken);
}
}
@@ -0,0 +1,6 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Cancel;
internal class Request
{
public string Id { get; set; } = default!;
}
@@ -0,0 +1,27 @@
using Elsa.Abstractions;
using Elsa.Workflows.Management;
using Elsa.Workflows.Management.Filters;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Delete;
[PublicAPI]
internal class Delete(IWorkflowInstanceManager store) : ElsaEndpoint<Request>
{
public override void Configure()
{
Delete("/workflow-instances/{id}");
ConfigurePermissions("delete:workflow-instances");
}
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var filter = new WorkflowInstanceFilter { Id = request.Id };
var deleted = await store.DeleteAsync(filter, cancellationToken);
if (deleted)
await SendNoContentAsync(cancellationToken);
else
await SendNotFoundAsync(cancellationToken);
}
}

Some files were not shown because too many files have changed in this diff Show More