Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanced Workflow Instance Filters #4957

Merged
merged 9 commits into from
Feb 18, 2024
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Linq.Dynamic.Core" Version="1.3.8" />
<PackageVersion Include="Testcontainers" Version="3.6.0" />
<PackageVersion Include="Testcontainers.Redis" Version="3.6.0" />
<PackageVersion Include="ThrottleDebounce" Version="2.0.0" />
Expand Down
10 changes: 8 additions & 2 deletions NuGet.Config
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
<packageSources>
<clear />
<add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
<!--
<add key="Elsa 3 Preview" value="https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json" />
-->
</packageSources>
<packageSourceMapping>
<packageSource key="NuGet official package source">
<package pattern="*" />
</packageSource>
<packageSource key="Elsa 3 Preview">
<package pattern="Elsa.*" />
</packageSource>
</packageSourceMapping>
</configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public interface IWorkflowInstancesApi
/// <summary>
/// Returns a list of workflow instances.
/// </summary>
[Get("/workflow-instances")]
[Post("/workflow-instances")]
Task<PagedListResponse<WorkflowInstanceSummary>> ListAsync(ListWorkflowInstancesRequest request, CancellationToken cancellationToken = default);

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,64 +13,74 @@ public class ListWorkflowInstancesRequest
/// Gets or sets the page number.
/// </summary>
public int? Page { get; set; }

/// <summary>
/// Gets or sets the page size.
/// </summary>
public int? PageSize { get; set; }

/// <summary>
/// Gets or sets the search term.
/// </summary>
public string? SearchTerm { get; set; }

/// <summary>
/// Gets or sets the workflow definition id.
/// </summary>
public string? DefinitionId { get; set; }

/// <summary>
/// Gets or sets the workflow definition ids.
/// </summary>
[Query(CollectionFormat.Multi)] public ICollection<string>? DefinitionIds { get; set; }
public ICollection<string>? DefinitionIds { get; set; }

/// <summary>
/// Gets or sets the correlation id.
/// </summary>
public string? CorrelationId { get; set; }

/// <summary>
/// Gets or sets the version.
/// </summary>
public int? Version { get; set; }


/// <summary>
/// Gets or sets whether the workflow instances have incidents or not.
/// </summary>
public bool? HasIncidents { get; set; }
sfmskywalker marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets or sets the status.
/// </summary>
public WorkflowStatus? Status { get; set; }

/// <summary>
/// Gets or sets the statuses.
/// </summary>
[Query(CollectionFormat.Multi)] public ICollection<WorkflowStatus>? Statuses { get; set; }
public ICollection<WorkflowStatus>? Statuses { get; set; }

/// <summary>
/// Gets or sets the sub status.
/// </summary>
public WorkflowSubStatus? SubStatus { get; set; }

/// <summary>
/// Gets or sets the sub statuses.
/// </summary>
[Query(CollectionFormat.Multi)] public ICollection<WorkflowSubStatus>? SubStatuses { get; set; }
public ICollection<WorkflowSubStatus>? SubStatuses { get; set; }

/// <summary>
/// Gets or sets the key to order by.
/// </summary>
public OrderByWorkflowInstance? OrderBy { get; set; }

/// <summary>
/// Gets or sets the order direction.
/// </summary>
public OrderDirection? OrderDirection { get; set; }

/// <summary>
/// Gets or sets the timestamp filters.
/// </summary>
public ICollection<TimestampFilter>? TimestampFilters { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Elsa.Api.Client.Shared.Enums;

/// <summary>
/// Specifies the operators for filtering timestamps in a TimestampFilter object.
/// </summary>
public enum TimestampFilterOperator
{
/// <summary>
/// The timestamp is equal to the specified value.
/// </summary>
Is,

/// <summary>
/// The timestamp is not equal to the specified value.
/// </summary>
IsNot,

/// <summary>
/// The timestamp is before the specified value.
/// </summary>
LessThan,

/// <summary>
/// The timestamp is greater than the specified value.
/// </summary>
GreaterThan,

/// <summary>
/// The timestamp is less than or equal to the specified value.
/// </summary>
LessThanOrEqual,

/// <summary>
/// The timestamp is greater than or equal to the specified value.
/// </summary>
GreaterThanOrEqual
}
26 changes: 26 additions & 0 deletions src/clients/Elsa.Api.Client/Shared/Models/TimestampFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Elsa.Api.Client.Shared.Enums;
using JetBrains.Annotations;

namespace Elsa.Api.Client.Shared.Models;

/// <summary>
/// Represents a timestamp filter used for filtering data based on a specified timestamp column and operator.
/// </summary>
[UsedImplicitly]
public class TimestampFilter
{
/// <summary>
/// Gets or sets the column to filter by.
/// </summary>
public string Column { get; set; } = default!;
sfmskywalker marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets or sets the operator to use for filtering.
/// </summary>
public TimestampFilterOperator Operator { get; set; }

/// <summary>
/// Gets or sets the timestamp to filter by.
/// </summary>
public DateTimeOffset Timestamp { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,65 +6,106 @@
using Elsa.Workflows.Management.Filters;
using Elsa.Workflows.Management.Models;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;

namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.List;

[PublicAPI]
internal class List : ElsaEndpoint<Request, Response>
[UsedImplicitly]
internal class List(IWorkflowInstanceStore store) : ElsaEndpoint<Request, Response>
{
private readonly IWorkflowInstanceStore _store;

public List(IWorkflowInstanceStore store)
{
_store = store;
}

public override void Configure()
{
Get("/workflow-instances");
Verbs(FastEndpoints.Http.GET, FastEndpoints.Http.POST);
Routes("/workflow-instances");
ConfigurePermissions("read:workflow-instances");
}

public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
public override async Task HandleAsync(Request request, CancellationToken cancellationToken)
{
var pageArgs = PageArgs.FromPage(request.Page, request.PageSize);

if (!await ValidateInputAsync(request, cancellationToken))
{
await SendErrorsAsync(StatusCodes.Status400BadRequest, cancellationToken);
return;
}

var filter = new WorkflowInstanceFilter
{
SearchTerm = request.SearchTerm,
DefinitionId = request.DefinitionId,
DefinitionIds = request.DefinitionIds,
DefinitionIds = request.DefinitionIds?.Any() == true ? request.DefinitionIds : null,
Version = request.Version,
CorrelationId = request.CorrelationId,
WorkflowStatus = request.Status,
WorkflowSubStatus = request.SubStatus,
WorkflowStatuses = request.Statuses?.Select(Enum.Parse<WorkflowStatus>).ToList(),
WorkflowSubStatuses = request.SubStatuses?.Select(Enum.Parse<WorkflowSubStatus>).ToList(),
ToCreatedAt = request.ToCreatedAt,
FromCreatedAt = request.FromCreatedAt,
WorkflowStatuses = request.Statuses?.Any() == true ? request.Statuses : null,
WorkflowSubStatuses = request.SubStatuses?.Any() == true ? request.SubStatuses : null,
HasIncidents = request.HasIncidents,
TimestampFilters = request.TimestampFilters?.Any() == true ? request.TimestampFilters : null,
sfmskywalker marked this conversation as resolved.
Show resolved Hide resolved
};

var summaries = await FindAsync(request, filter, pageArgs, cancellationToken);
return new Response(summaries.Items, summaries.TotalCount);
var response = new Response(summaries.Items, summaries.TotalCount);
await SendOkAsync(response, cancellationToken);
}

private async Task<bool> ValidateInputAsync(Request request, CancellationToken cancellationToken)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason you are not using the validation engine of fastendpoints?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure how to do it for the TimestampFilters collection, but maybe I missed something obvious. If you have a suggestion then it'd be great to move it into its own DTO validator 👍🏻

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use custom functions for validation as well. I plan to have a PR up later today in which I use this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see - I will give that a try, thank you 👍🏻

{
if (request.Page is < 0)
{
AddError("Page must be greater than or equal to 1.");
return false;
}

if (request.PageSize is < 1)
{
AddError("Page size must be greater than or equal to 1.");
return false;
}

var columnWhitelist = new[]
{
"CreatedAt", "UpdatedAt", "FinishedAt"
};

if (request.TimestampFilters?.Any() == true)
{
foreach (var timestampFilter in request.TimestampFilters)
{
if (string.IsNullOrWhiteSpace(timestampFilter.Column))
{
AddError("Column must be specified.");
return false;
}

if (!columnWhitelist.Contains(timestampFilter.Column))
{
AddError($"Invalid column '{timestampFilter.Column}'.");
return false;
}
}
}

return true;
}

private async Task<Page<WorkflowInstanceSummary>> FindAsync(Request request, WorkflowInstanceFilter filter, PageArgs pageArgs, CancellationToken cancellationToken)
{
request.OrderBy = request.OrderBy ?? OrderByWorkflowInstance.Created;
var direction = request.OrderBy == OrderByWorkflowInstance.Name ? (request.OrderDirection ?? OrderDirection.Ascending) : (request.OrderDirection ?? OrderDirection.Descending);
request.OrderBy ??= OrderByWorkflowInstance.Created;
var direction = request.OrderBy == OrderByWorkflowInstance.Name ? request.OrderDirection ?? OrderDirection.Ascending : request.OrderDirection ?? OrderDirection.Descending;

switch (request.OrderBy)
{
default:
case OrderByWorkflowInstance.Created:
{
var o = new WorkflowInstanceOrder<DateTimeOffset>
{
KeySelector = p => p.CreatedAt,
Direction = direction
};

return await _store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
return await store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
}
case OrderByWorkflowInstance.UpdatedAt:
{
Expand All @@ -74,7 +115,7 @@ private async Task<Page<WorkflowInstanceSummary>> FindAsync(Request request, Wor
Direction = direction
};

return await _store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
return await store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
}
case OrderByWorkflowInstance.Finished:
{
Expand All @@ -84,7 +125,7 @@ private async Task<Page<WorkflowInstanceSummary>> FindAsync(Request request, Wor
Direction = direction
};

return await _store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
return await store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
}
case OrderByWorkflowInstance.Name:
{
Expand All @@ -94,7 +135,7 @@ private async Task<Page<WorkflowInstanceSummary>> FindAsync(Request request, Wor
Direction = direction
};

return await _store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
return await store.SummarizeManyAsync(filter, pageArgs, o, cancellationToken);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,21 @@ public class Request
public string? DefinitionId { get; set; }
public ICollection<string>? DefinitionIds { get; set; }
public string? CorrelationId { get; set; }
public string? Name { get; set; }
public int? Version { get; set; }
public bool? HasIncidents { get; set; }
public WorkflowStatus? Status { get; set; }
public ICollection<string>? Statuses { get; set; }
public ICollection<WorkflowStatus>? Statuses { get; set; }
public WorkflowSubStatus? SubStatus { get; set; }
public ICollection<string>? SubStatuses { get; set; }
public ICollection<WorkflowSubStatus>? SubStatuses { get; set; }
public OrderByWorkflowInstance? OrderBy { get; set; }
public OrderDirection? OrderDirection { get; set; }
public DateTimeOffset? FromCreatedAt { get; set; }
public DateTimeOffset? ToCreatedAt { get; set; }
public ICollection<TimestampFilter>? TimestampFilters { get; set; }
}

public class Response
internal class Response(ICollection<WorkflowInstanceSummary> items, long totalCount)
{
public Response(ICollection<WorkflowInstanceSummary> items, long totalCount)
{
Items = items;
TotalCount = totalCount;
}
public ICollection<WorkflowInstanceSummary> Items { get; set; } = items;
public long TotalCount { get; set; } = totalCount;
}

public ICollection<WorkflowInstanceSummary> Items { get; set; }
public long TotalCount { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="Humanizer.Core" />
<PackageReference Include="IronCompress" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="System.Linq.Dynamic.Core" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading