Add project files.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Elsa.Workflows.IntegrationTests")]
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Elsa.Workflows.Api.Constants;
|
||||
|
||||
public static class AuthorizationPolicies
|
||||
{
|
||||
/// <summary>
|
||||
/// The read-only policy ensures that that workflow management is not possible when the read-only mode is enabled or when a workflow is marked as read-only.
|
||||
/// </summary>
|
||||
public const string NotReadOnlyPolicy = "NotReadOnlyPolicy";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Elsa.Models;
|
||||
using Elsa.Workflows.Api.Models;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
|
||||
namespace Elsa.Workflows.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Maps workflow definition models to liked models
|
||||
/// </summary>
|
||||
public interface IWorkflowDefinitionLinker
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps to an enhanced model that contains links with the possible operations applicable to a workflow definition.
|
||||
/// </summary>
|
||||
Task<LinkedWorkflowDefinitionModel> MapAsync(WorkflowDefinition definition, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a paged list to an enhanced model that contains links with the possible operations applicable to a workflow definition.
|
||||
/// </summary>
|
||||
PagedListResponse<LinkedWorkflowDefinitionSummary> MapAsync(PagedListResponse<WorkflowDefinitionSummary> list, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Maps a list to an enhanced model that contains links with the possible operations applicable to a workflow definition.
|
||||
/// </summary>
|
||||
Task<List<LinkedWorkflowDefinitionModel>> MapAsync(List<WorkflowDefinition> definitions, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides API endpoints for client applications such as the Elsa Designer web component.
|
||||
</Description>
|
||||
<PackageTags>elsa module workflows api</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj" />
|
||||
<ProjectReference Include="..\Elsa.JavaScript\Elsa.JavaScript.csproj" />
|
||||
<ProjectReference Include="..\Elsa.SasTokens\Elsa.SasTokens.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Endpoints\CommitStrategies\Workflows\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=options/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -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;
|
||||
}
|
||||
+38
@@ -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);
|
||||
}
|
||||
}
|
||||
+22
@@ -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);
|
||||
+45
@@ -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);
|
||||
+43
@@ -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;
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
}
|
||||
+13
@@ -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!;
|
||||
}
|
||||
+37
@@ -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!;
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
}
|
||||
+7
@@ -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!;
|
||||
}
|
||||
+68
@@ -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!;
|
||||
}
|
||||
+52
@@ -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);
|
||||
}
|
||||
}
|
||||
+8
@@ -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;
|
||||
}
|
||||
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user