From 750320082caaf34871eadf26d91b269c8e35aa9b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 24 May 2022 20:51:12 +0200 Subject: [PATCH] V3 Bulk Actions API Endpoints (#3064) * Add bulk-delete API for workflow instances * Add bulk-cancel API for workflow instances (stub) * Add bulk-delete API for workflow definitions * Add bulk-publish API for workflow definitions * Add bulk-retract API for workflow definitions --- .../WorkflowDefinitions/BulkDelete.cs | 39 +++++++++++ .../WorkflowDefinitions/BulkPublish.cs | 65 +++++++++++++++++++ .../WorkflowDefinitions/BulkRetract.cs | 65 +++++++++++++++++++ .../Endpoints/WorkflowDefinitions/Retract.cs | 6 ++ .../Endpoints/WorkflowInstances/BulkCancel.cs | 39 +++++++++++ .../Endpoints/WorkflowInstances/BulkDelete.cs | 40 ++++++++++++ .../EndpointRouteBuilderExtensions.cs | 7 +- 7 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkDelete.cs create mode 100644 src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkPublish.cs create mode 100644 src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkRetract.cs create mode 100644 src/api/Elsa.Api/Endpoints/WorkflowInstances/BulkCancel.cs create mode 100644 src/api/Elsa.Api/Endpoints/WorkflowInstances/BulkDelete.cs diff --git a/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkDelete.cs b/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkDelete.cs new file mode 100644 index 000000000..1fed4a0a6 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkDelete.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Elsa.AspNetCore; +using Elsa.Persistence.Services; +using Elsa.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Elsa.Api.Endpoints.WorkflowDefinitions; + +[Area(AreaNames.Elsa)] +[ApiEndpoint(ControllerNames.WorkflowDefinitions, "BulkDelete")] +public class BulkDelete : Controller +{ + private readonly IWorkflowDefinitionStore _store; + private readonly WorkflowSerializerOptionsProvider _serializerOptionsProvider; + + public BulkDelete(IWorkflowDefinitionStore store, WorkflowSerializerOptionsProvider serializerOptionsProvider) + { + _store = store; + _serializerOptionsProvider = serializerOptionsProvider; + } + + [HttpPost] + public async Task HandleAsync(CancellationToken cancellationToken) + { + var serializerOptions = _serializerOptionsProvider.CreateApiOptions(); + var model = await Request.ReadFromJsonAsync(serializerOptions, cancellationToken); + var count = await _store.DeleteManyByDefinitionIdsAsync(model!.DefinitionIds, cancellationToken); + + return Json(new BulkDeleteWorkflowDefinitionsResponse(count), serializerOptions); + } + + public record BulkDeleteWorkflowDefinitionsRequest(ICollection DefinitionIds); + + public record BulkDeleteWorkflowDefinitionsResponse([property: JsonPropertyName("deleted")] int DeletedCount); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkPublish.cs b/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkPublish.cs new file mode 100644 index 000000000..cc88061f8 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkPublish.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Elsa.AspNetCore; +using Elsa.Management.Services; +using Elsa.Persistence.Models; +using Elsa.Persistence.Services; +using Elsa.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Elsa.Api.Endpoints.WorkflowDefinitions; + +[Area(AreaNames.Elsa)] +[ApiEndpoint(ControllerNames.WorkflowDefinitions, "BulkPublish")] +public class BulkPublish : Controller +{ + private readonly IWorkflowDefinitionStore _store; + private readonly IWorkflowPublisher _workflowPublisher; + private readonly WorkflowSerializerOptionsProvider _serializerOptionsProvider; + + public BulkPublish(IWorkflowDefinitionStore store, IWorkflowPublisher workflowPublisher, WorkflowSerializerOptionsProvider serializerOptionsProvider) + { + _store = store; + _workflowPublisher = workflowPublisher; + _serializerOptionsProvider = serializerOptionsProvider; + } + + [HttpPost] + public async Task HandleAsync(CancellationToken cancellationToken) + { + var serializerOptions = _serializerOptionsProvider.CreateApiOptions(); + var model = (await Request.ReadFromJsonAsync(serializerOptions, cancellationToken))!; + var published = new List(); + var notFound = new List(); + var alreadyPublished = new List(); + + foreach (var definitionId in model.DefinitionIds) + { + var definition = await _store.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken); + + if (definition == null) + { + notFound.Add(definitionId); + continue; + } + + if (definition.IsPublished) + { + alreadyPublished.Add(definitionId); + continue; + } + + await _workflowPublisher.PublishAsync(definition, cancellationToken); + published.Add(definitionId); + } + + return Json(new BulkPublishWorkflowDefinitionsResponse(published, alreadyPublished, notFound), serializerOptions); + } + + public record BulkPublishWorkflowDefinitionsRequest(ICollection DefinitionIds); + + public record BulkPublishWorkflowDefinitionsResponse(ICollection Published, ICollection AlreadyPublished, ICollection NotFound); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkRetract.cs b/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkRetract.cs new file mode 100644 index 000000000..f0ac1269a --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/BulkRetract.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Elsa.AspNetCore; +using Elsa.Management.Services; +using Elsa.Persistence.Models; +using Elsa.Persistence.Services; +using Elsa.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Elsa.Api.Endpoints.WorkflowDefinitions; + +[Area(AreaNames.Elsa)] +[ApiEndpoint(ControllerNames.WorkflowDefinitions, "BulkRetract")] +public class BulkRetract : Controller +{ + private readonly IWorkflowDefinitionStore _store; + private readonly IWorkflowPublisher _workflowPublisher; + private readonly WorkflowSerializerOptionsProvider _serializerOptionsProvider; + + public BulkRetract(IWorkflowDefinitionStore store, IWorkflowPublisher workflowPublisher, WorkflowSerializerOptionsProvider serializerOptionsProvider) + { + _store = store; + _workflowPublisher = workflowPublisher; + _serializerOptionsProvider = serializerOptionsProvider; + } + + [HttpPost] + public async Task HandleAsync(CancellationToken cancellationToken) + { + var serializerOptions = _serializerOptionsProvider.CreateApiOptions(); + var model = (await Request.ReadFromJsonAsync(serializerOptions, cancellationToken))!; + var retracted = new List(); + var notFound = new List(); + var notPublished = new List(); + + foreach (var definitionId in model.DefinitionIds) + { + var definition = await _store.FindByDefinitionIdAsync(definitionId, VersionOptions.Latest, cancellationToken); + + if (definition == null) + { + notFound.Add(definitionId); + continue; + } + + if (!definition.IsPublished) + { + notPublished.Add(definitionId); + continue; + } + + await _workflowPublisher.RetractAsync(definition, cancellationToken); + retracted.Add(definitionId); + } + + return Json(new BulkRetractWorkflowDefinitionsResponse(retracted, notPublished, notFound), serializerOptions); + } + + public record BulkRetractWorkflowDefinitionsRequest(ICollection DefinitionIds); + + public record BulkRetractWorkflowDefinitionsResponse(ICollection Retracted, ICollection NotPublished, ICollection NotFound); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/Retract.cs b/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/Retract.cs index 2fee5b19a..71ba4a9d7 100644 --- a/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/Retract.cs +++ b/src/api/Elsa.Api/Endpoints/WorkflowDefinitions/Retract.cs @@ -36,6 +36,12 @@ public class Retract : Controller if (definition == null) return NotFound(); + + if (!definition.IsPublished) + return BadRequest(new + { + Message = $"Workflow with id {definitionId} is not published" + }); await _workflowPublisher.RetractAsync(definition, cancellationToken); var serializerOptions = _serializerOptionsProvider.CreateApiOptions(); diff --git a/src/api/Elsa.Api/Endpoints/WorkflowInstances/BulkCancel.cs b/src/api/Elsa.Api/Endpoints/WorkflowInstances/BulkCancel.cs new file mode 100644 index 000000000..27bed9bd0 --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/WorkflowInstances/BulkCancel.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Elsa.AspNetCore; +using Elsa.Persistence.Services; +using Elsa.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Elsa.Api.Endpoints.WorkflowInstances; + +[Area(AreaNames.Elsa)] +[ApiEndpoint(ControllerNames.WorkflowInstances, "BulkCancel")] +public class BulkCancel : Controller +{ + private readonly WorkflowSerializerOptionsProvider _serializerOptionsProvider; + + public BulkCancel(WorkflowSerializerOptionsProvider serializerOptionsProvider) + { + _serializerOptionsProvider = serializerOptionsProvider; + } + + [HttpPost] + public async Task HandleAsync(CancellationToken cancellationToken) + { + var serializerOptions = _serializerOptionsProvider.CreateApiOptions(); + var model = await Request.ReadFromJsonAsync(serializerOptions, cancellationToken); + // TODO: Implement workflow cancellation. + var count = -1; + + return Json(new BulkCancelWorkflowInstancesResponse(count), serializerOptions); + } + + public record BulkCancelWorkflowInstancesRequest(ICollection Ids); + + public record BulkCancelWorkflowInstancesResponse([property: JsonPropertyName("cancelled")] int CancelledCount); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Endpoints/WorkflowInstances/BulkDelete.cs b/src/api/Elsa.Api/Endpoints/WorkflowInstances/BulkDelete.cs new file mode 100644 index 000000000..0d1d0207d --- /dev/null +++ b/src/api/Elsa.Api/Endpoints/WorkflowInstances/BulkDelete.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Elsa.AspNetCore; +using Elsa.Persistence.Services; +using Elsa.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Elsa.Api.Endpoints.WorkflowInstances; + +[Area(AreaNames.Elsa)] +[ApiEndpoint(ControllerNames.WorkflowInstances, "BulkDelete")] +public class BulkDelete : Controller +{ + private readonly IWorkflowInstanceStore _store; + private readonly WorkflowSerializerOptionsProvider _serializerOptionsProvider; + + public BulkDelete(IWorkflowInstanceStore store, WorkflowSerializerOptionsProvider serializerOptionsProvider) + { + _store = store; + _serializerOptionsProvider = serializerOptionsProvider; + } + + [HttpPost] + public async Task HandleAsync(CancellationToken cancellationToken) + { + var serializerOptions = _serializerOptionsProvider.CreateApiOptions(); + var model = await Request.ReadFromJsonAsync(serializerOptions, cancellationToken); + var count = await _store.DeleteManyAsync(model!.Ids, cancellationToken); + + return Json(new BulkDeleteWorkflowInstancesResponse(count), serializerOptions); + } + + public record BulkDeleteWorkflowInstancesRequest(ICollection Ids); + + public record BulkDeleteWorkflowInstancesResponse([property: JsonPropertyName("deleted")] int DeletedCount); +} \ No newline at end of file diff --git a/src/api/Elsa.Api/Extensions/EndpointRouteBuilderExtensions.cs b/src/api/Elsa.Api/Extensions/EndpointRouteBuilderExtensions.cs index 3d82c25d6..0856dd634 100644 --- a/src/api/Elsa.Api/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/api/Elsa.Api/Extensions/EndpointRouteBuilderExtensions.cs @@ -30,8 +30,13 @@ public static class EndpointRouteBuilderExtensions Map("WorkflowDefinitions.Dispatch", "workflow-definitions/{definitionId}/dispatch", new { Controller = ControllerNames.WorkflowDefinitions, Action = "Dispatch" }); Map("WorkflowDefinitions.Execute", "workflow-definitions/{definitionId}/execute", new { Controller = ControllerNames.WorkflowDefinitions, Action = "Execute" }); Map("WorkflowDefinitions.Export", "workflow-definitions/{definitionId}/export", new { Controller = ControllerNames.WorkflowDefinitions, Action = "Export" }); + Map("WorkflowDefinitions.BulkDelete", "bulk-actions/delete/workflow-definitions/by-definition-id", new { Controller = ControllerNames.WorkflowDefinitions, Action = "BulkDelete" }); + Map("WorkflowDefinitions.BulkPublish", "bulk-actions/publish/workflow-definitions/by-definition-id", new { Controller = ControllerNames.WorkflowDefinitions, Action = "BulkPublish" }); + Map("WorkflowDefinitions.BulkRetract", "bulk-actions/retract/workflow-definitions/by-definition-id", new { Controller = ControllerNames.WorkflowDefinitions, Action = "BulkRetract" }); // Workflow Instances. + Map("WorkflowInstances.BulkDelete", "bulk-actions/delete/workflow-instances/by-id", new { Controller = ControllerNames.WorkflowInstances, Action = "BulkDelete" }); + Map("WorkflowInstances.BulkCancel", "bulk-actions/cancel/workflow-instances/by-id", new { Controller = ControllerNames.WorkflowInstances, Action = "BulkCancel" }); Map("WorkflowInstances.Get", "workflow-instances/{id}", new { Controller = ControllerNames.WorkflowInstances, Action = "Get" }); Map("WorkflowInstances.Delete", "workflow-instances/{id}", new { Controller = ControllerNames.WorkflowInstances, Action = "Delete" }); Map("WorkflowInstances.List", "workflow-instances", new { Controller = ControllerNames.WorkflowInstances, Action = "List" }); @@ -42,7 +47,7 @@ public static class EndpointRouteBuilderExtensions Map("Labels.Get", "labels/{id}", new { Controller = ControllerNames.Labels, Action = "Get" }); Map("Labels.Delete", "labels/{id}", new { Controller = ControllerNames.Labels, Action = "Delete" }); Map("Labels.List", "labels", new { Controller = ControllerNames.Labels, Action = "List" }); - + // Workflow Definition Labels Map("WorkflowDefinitions.Labels.Update", "workflow-definitions/{id}/labels", new { Controller = ControllerNames.WorkflowDefinitionLabels, Action = "Update" }); Map("WorkflowDefinitions.Labels.Get", "workflow-definitions/{id}/labels", new { Controller = ControllerNames.WorkflowDefinitionLabels, Action = "Get" });