Add support for filtering journal by activity IDs

This commit is contained in:
Sipke Schoorstra 2023-07-20 22:12:45 +02:00
parent 142af4e3b9
commit 1902307cf5
19 changed files with 285 additions and 33 deletions

View file

@ -21,7 +21,7 @@ public interface IWorkflowDefinitionsApi
/// <param name="versionOptions">The version options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
[Get("/workflow-definitions?versionOptions={versionOptions}")]
Task<ListWorkflowDefinitionsResponse> ListAsync([Query]ListWorkflowDefinitionsRequest request, [Query]VersionOptions? versionOptions = default, CancellationToken cancellationToken = default);
Task<PagedListResponse<WorkflowDefinitionSummary>> ListAsync([Query]ListWorkflowDefinitionsRequest request, [Query]VersionOptions? versionOptions = default, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a workflow definition by definition ID.
@ -42,6 +42,15 @@ public interface IWorkflowDefinitionsApi
[Get("/workflow-definitions/by-id/{id}?includeCompositeRoot={includeCompositeRoot}")]
Task<WorkflowDefinition?> GetByIdAsync(string id, bool includeCompositeRoot = false, CancellationToken cancellationToken = default);
/// <summary>
/// Gets a workflow definition by ID.
/// </summary>
/// <param name="ids">The IDs of the workflow definition versions to get.</param>
/// <param name="includeCompositeRoot">Whether to include the root activity of composite activities.</param>
/// <param name="cancellationToken">The cancellation token.</param>
[Get("/workflow-definitions/many-by-id")]
Task<ListResponse<WorkflowDefinition>> GetManyByIdAsync([Query(CollectionFormat.Multi)]ICollection<string> ids, bool includeCompositeRoot = false, CancellationToken cancellationToken = default);
/// <summary>
/// Gets the number of workflow definitions.
/// </summary>

View file

@ -1,10 +0,0 @@
using Elsa.Api.Client.Resources.WorkflowDefinitions.Models;
namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Responses;
/// <summary>
/// Represents a response from listing workflow definitions.
/// </summary>
/// <param name="Items">The workflow definitions.</param>
/// <param name="TotalCount">The total number of workflow definitions.</param>
public record ListWorkflowDefinitionsResponse(ICollection<WorkflowDefinitionSummary> Items, int TotalCount);

View file

@ -25,7 +25,7 @@ public interface IWorkflowInstancesApi
Task<WorkflowInstance> GetAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Returns a workflow instance.
/// Returns a page of journal records for the specified workflow instance.
/// </summary>
/// <param name="workflowInstanceId">The ID of the workflow instance for which to return the journal.</param>
/// <param name="skip">The number of records to skip.</param>
@ -34,6 +34,17 @@ public interface IWorkflowInstancesApi
[Get("/workflow-instances/{workflowInstanceId}/journal")]
Task<PagedListResponse<ExecutionLogRecord>> GetJournalAsync(string workflowInstanceId, int? skip = default, int? take = default, CancellationToken cancellationToken = default);
/// <summary>
/// Returns a page of journal records for the specified workflow instance.
/// </summary>
/// <param name="workflowInstanceId">The ID of the workflow instance for which to return the journal.</param>
/// <param name="filter">The filter to apply.</param>
/// <param name="skip">The number of records to skip.</param>
/// <param name="take">The number of records to return.</param>
/// <param name="cancellationToken">The cancellation token.</param>
[Post("/workflow-instances/{workflowInstanceId}/journal")]
Task<PagedListResponse<ExecutionLogRecord>> GetFilteredJournalAsync(string workflowInstanceId, GetFilteredJournalRequest? filter, int? skip = default, int? take = default, CancellationToken cancellationToken = default);
/// <summary>
/// Deletes a workflow instance.
/// </summary>

View file

@ -0,0 +1,12 @@
namespace Elsa.Api.Client.Resources.WorkflowInstances.Requests;
/// <summary>
/// Represents a request to list journal records.
/// </summary>
public class GetFilteredJournalRequest
{
/// <summary>
/// Gets or sets the filter to apply.
/// </summary>
public JournalFilter? Filter { get; set; }
}

View file

@ -0,0 +1,13 @@
namespace Elsa.Api.Client.Resources.WorkflowInstances.Requests;
/// <summary>
/// Represents a request to list journal records.
/// </summary>
public class JournalFilter
{
/// <summary>
/// Gets or sets the activity ids to filter by.
/// </summary>
public ICollection<string>? ActivityIds { get; set; }
}

View file

@ -34,8 +34,8 @@ public record PageArgs
if(offset != null && limit != null)
return FromRange(offset, limit);
throw new ArgumentException("Either page and pageSize or offset and limit must be specified.");
return FromPage(0, 100);
}
/// <summary>

View file

@ -54,6 +54,7 @@ public class DapperBookmarkStore : IBookmarkStore
query
.Is(nameof(StoredBookmarkRecord.Hash), filter.Hash)
.Is(nameof(StoredBookmarkRecord.WorkflowInstanceId), filter.WorkflowInstanceId)
.In(nameof(StoredBookmarkRecord.WorkflowInstanceId), filter.WorkflowInstanceIds)
.Is(nameof(StoredBookmarkRecord.CorrelationId), filter.CorrelationId)
.Is(nameof(StoredBookmarkRecord.ActivityTypeName), filter.ActivityTypeName)
.In(nameof(StoredBookmarkRecord.ActivityTypeName), filter.ActivityTypeNames)

View file

@ -71,6 +71,8 @@ public class DapperTriggerStore : ITriggerStore
.In(nameof(StoredTriggerRecord.Id), filter.Ids)
.Is(nameof(StoredTriggerRecord.WorkflowDefinitionId), filter.WorkflowDefinitionId)
.In(nameof(StoredTriggerRecord.WorkflowDefinitionId), filter.WorkflowDefinitionIds)
.Is(nameof(StoredTriggerRecord.WorkflowDefinitionVersionId), filter.WorkflowDefinitionVersionId)
.In(nameof(StoredTriggerRecord.WorkflowDefinitionVersionId), filter.WorkflowDefinitionVersionIds)
.Is(nameof(StoredTriggerRecord.Name), filter.Name)
.In(nameof(StoredTriggerRecord.Name), filter.Names)
.Is(nameof(StoredTriggerRecord.Hash), filter.Hash)

View file

@ -89,7 +89,9 @@ public class DapperWorkflowExecutionLogStore : IWorkflowExecutionLogStore
query
.Is(nameof(WorkflowExecutionLogRecordRecord.Id), filter.Id)
.In(nameof(WorkflowExecutionLogRecordRecord.Id), filter.Ids)
.Is(nameof(WorkflowExecutionLogRecordRecord.ParentActivityInstanceId), filter.ParentActivityInstanceId)
.Is(nameof(WorkflowExecutionLogRecordRecord.ActivityId), filter.ActivityId)
.In(nameof(WorkflowExecutionLogRecordRecord.ActivityId), filter.ActivityIds)
.Is(nameof(WorkflowExecutionLogRecordRecord.WorkflowInstanceId), filter.WorkflowInstanceId)
.In(nameof(WorkflowExecutionLogRecordRecord.WorkflowInstanceId), filter.WorkflowInstanceIds)
.Is(nameof(WorkflowExecutionLogRecordRecord.EventName), filter.EventName)

View file

@ -36,7 +36,7 @@ internal class GetById : ElsaEndpoint<Request>
Id = request.Id
};
var definition = (await _store.FindManyAsync(filter, cancellationToken)).FirstOrDefault();
var definition = await _store.FindAsync(filter, cancellationToken);
if (definition == null)
{

View file

@ -0,0 +1,52 @@
using Elsa.Abstractions;
using Elsa.Models;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Serialization.Converters;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetManyById;
[PublicAPI]
internal class GetManyById : ElsaEndpoint<Request>
{
private readonly IWorkflowDefinitionStore _store;
private readonly IApiSerializer _apiSerializer;
private readonly WorkflowDefinitionMapper _mapper;
public GetManyById(IWorkflowDefinitionStore store, IApiSerializer apiSerializer, WorkflowDefinitionMapper mapper)
{
_store = store;
_apiSerializer = apiSerializer;
_mapper = mapper;
}
public override void Configure()
{
Get("/workflow-definitions/many-by-id");
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 _mapper.MapAsync(definitions, cancellationToken)).ToList();
var serializerOptions = _apiSerializer.CreateOptions();
// If the root of composite activities is not requested, exclude them from being serialized.
if (!request.IncludeCompositeRoot)
serializerOptions.Converters.Add(new JsonIgnoreCompositeRootConverterFactory());
var response = new ListResponse<WorkflowDefinitionModel>(models);
await HttpContext.Response.WriteAsJsonAsync(response, serializerOptions, cancellationToken);
}
}

View file

@ -0,0 +1,11 @@
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.GetManyById;
internal class Request
{
public ICollection<string> Ids { get; set; } = default!;
/// <summary>
/// True if the response should include the root activity of composite activities.
/// </summary>
public bool IncludeCompositeRoot { get; set; }
}

View file

@ -1,6 +1,7 @@
using Elsa.Abstractions;
using Elsa.Common.Entities;
using Elsa.Common.Models;
using Elsa.Models;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Filters;
@ -10,7 +11,7 @@ using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.List;
[PublicAPI]
internal class List : ElsaEndpoint<Request, Response>
internal class List : ElsaEndpoint<Request, PagedListResponse<WorkflowDefinitionSummary>>
{
private readonly IWorkflowDefinitionStore _store;
@ -25,13 +26,12 @@ internal class List : ElsaEndpoint<Request, Response>
ConfigurePermissions("read:workflow-definitions");
}
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
public override async Task<PagedListResponse<WorkflowDefinitionSummary>> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var pageArgs = PageArgs.FromPage(request.Page, request.PageSize);
var filter = CreateFilter(request);
var summaries = await FindAsync(request, filter, pageArgs, cancellationToken);
return new Response(summaries.Items, summaries.TotalCount);
return new PagedListResponse<WorkflowDefinitionSummary>(summaries);
}
private WorkflowDefinitionFilter CreateFilter(Request request)

View file

@ -17,16 +17,4 @@ internal class Request
public OrderByWorkflowDefinition? OrderBy { get; set; }
public OrderDirection? OrderDirection { get; set; }
public string? SearchTerm { get; set; }
}
internal class Response
{
public Response(ICollection<WorkflowDefinitionSummary> items, long totalCount)
{
Items = items;
TotalCount = totalCount;
}
public ICollection<WorkflowDefinitionSummary> Items { get; set; }
public long TotalCount { get; set; }
}

View file

@ -1,9 +1,11 @@
using Elsa.Workflows.Management.Contracts;
using FastEndpoints;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowDefinitions.Version;
public class RevertVersion : EndpointWithoutRequest
[PublicAPI]
internal class RevertVersion : EndpointWithoutRequest
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;

View file

@ -0,0 +1,64 @@
using Elsa.Abstractions;
using Elsa.Common.Entities;
using Elsa.Common.Models;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.OrderDefinitions;
using JetBrains.Annotations;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Journal.FilteredList;
/// <summary>
/// Gets the journal for a workflow instance.
/// </summary>
[PublicAPI]
internal class Get : ElsaEndpoint<Request, Response>
{
private readonly IWorkflowExecutionLogStore _store;
/// <inheritdoc />
public Get(IWorkflowExecutionLogStore store)
{
_store = store;
}
/// <inheritdoc />
public override void Configure()
{
Post("/workflow-instances/{id}/journal");
ConfigurePermissions("read:workflow-instances");
}
/// <inheritdoc />
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var pageArgs = PageArgs.From(request.Page, request.PageSize, request.Skip, request.Take);
var filter = new WorkflowExecutionLogRecordFilter
{
WorkflowInstanceId = request.WorkflowInstanceId,
ActivityIds = request.Filter?.ActivityIds
};
var order = new WorkflowExecutionLogRecordOrder<long>(x => x.Sequence, OrderDirection.Ascending);
var pageOfRecords = await _store.FindManyAsync(filter, pageArgs, order, cancellationToken);
var models = pageOfRecords.Items.Select(x =>
new ExecutionLogRecord(
x.Id,
x.ActivityInstanceId,
x.ParentActivityInstanceId,
x.ActivityId,
x.ActivityType,
x.ActivityTypeVersion,
x.NodeId,
x.Timestamp,
x.Sequence,
x.EventName,
x.Message,
x.Source,
x.ActivityState,
x.Payload))
.ToList();
return new(models, pageOfRecords.TotalCount);
}
}

View file

@ -0,0 +1,74 @@
using FastEndpoints;
// ReSharper disable NotAccessedPositionalProperty.Global
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Journal.FilteredList;
/// <summary>
/// Represents a request for a page of workflow execution log records.
/// </summary>
internal class Request
{
/// <summary>
/// The ID of the workflow instance to get the execution log for.
/// </summary>
[BindFrom("id")] public string WorkflowInstanceId { get; set; } = default!;
/// <summary>
/// The filter to apply.
/// </summary>
public JournalFilter? Filter { get; set; }
/// <summary>
/// The zero-based page number to get.
/// </summary>
public int? Page { get; set; }
/// <summary>
/// The size of the page to get.
/// </summary>
public int? PageSize { get; set; }
/// <summary>
/// The number of records to skip.
/// </summary>
public int? Skip { get; set; }
/// <summary>
/// The number of records to take.
/// </summary>
public int? Take { get; set; }
}
internal class JournalFilter
{
public ICollection<string>? ActivityIds { get; set; }
}
internal class Response
{
public Response(ICollection<ExecutionLogRecord> items, long totalCount)
{
Items = items;
TotalCount = totalCount;
}
public ICollection<ExecutionLogRecord> Items { get; }
public long TotalCount { get; }
}
internal record ExecutionLogRecord(
string Id,
string ActivityInstanceId,
string? ParentActivityInstanceId,
string ActivityId,
string ActivityType,
int ActivityTypeVersion,
string NodeId,
DateTimeOffset Timestamp,
long Sequence,
string? EventName,
string? Message,
string? Source,
IDictionary<string, object>? ActivityState,
object? Payload);

View file

@ -80,6 +80,15 @@ public class WorkflowDefinitionMapper
source.IsReadonly);
}
/// <summary>
/// Maps many <see cref="WorkflowDefinition"/>s to many <see cref="WorkflowDefinitionModel"/>s.
/// </summary>
/// <param name="source">The source <see cref="WorkflowDefinition"/>s.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The mapped <see cref="WorkflowDefinitionModel"/>s.</returns>
public async Task<IEnumerable<WorkflowDefinitionModel>> MapAsync(IEnumerable<WorkflowDefinition> source, CancellationToken cancellationToken = default) =>
await Task.WhenAll(source.Select(async x => await MapAsync(x, cancellationToken)));
/// <summary>
/// Maps a <see cref="WorkflowDefinition"/> to a <see cref="Workflow"/>.
/// </summary>

View file

@ -27,10 +27,20 @@ public class WorkflowExecutionLogRecordFilter
/// </summary>
public ICollection<string>? WorkflowInstanceIds { get; set; }
/// <summary>
/// The ID of the parent activity instance.
/// </summary>
public string? ParentActivityInstanceId { get; set; }
/// <summary>
/// The ID of the activity.
/// </summary>
public string? ActivityId { get; set; }
/// <summary>
/// The IDs of the activities.
/// </summary>
public ICollection<string>? ActivityIds { get; set; }
/// <summary>
/// The name of the event.
@ -41,7 +51,7 @@ public class WorkflowExecutionLogRecordFilter
/// Match any of these event names.
/// </summary>
public ICollection<string>? AnyEventName { get; set; }
/// <summary>
/// Applies the filter to the specified queryable.
/// </summary>
@ -52,7 +62,9 @@ public class WorkflowExecutionLogRecordFilter
if (filter.Ids != null) queryable = queryable.Where(x => filter.Ids.Contains(x.Id!));
if (filter.WorkflowInstanceId != null) queryable = queryable.Where(x => x.WorkflowInstanceId == filter.WorkflowInstanceId);
if (filter.WorkflowInstanceIds != null) queryable = queryable.Where(x => filter.WorkflowInstanceIds.Contains(x.WorkflowInstanceId!));
if (filter.ParentActivityInstanceId != null) queryable = queryable.Where(x => x.ParentActivityInstanceId == filter.ParentActivityInstanceId);
if (filter.ActivityId != null) queryable = queryable.Where(x => x.ActivityId == filter.ActivityId);
if (filter.ActivityIds != null) queryable = queryable.Where(x => filter.ActivityIds.Contains(x.ActivityId));
if (filter.EventName != null) queryable = queryable.Where(x => x.EventName == filter.EventName);
if (filter.AnyEventName != null) queryable = queryable.Where(x => filter.AnyEventName.Contains(x.EventName!));