diff --git a/src/modules/Elsa.Dapper/Extensions/ParameterizedQueryBuilderExtensions.cs b/src/modules/Elsa.Dapper/Extensions/ParameterizedQueryBuilderExtensions.cs index f0583f08a..d988df328 100644 --- a/src/modules/Elsa.Dapper/Extensions/ParameterizedQueryBuilderExtensions.cs +++ b/src/modules/Elsa.Dapper/Extensions/ParameterizedQueryBuilderExtensions.cs @@ -107,7 +107,7 @@ public static class ParameterizedQueryBuilderExtensions return query; } - + /// /// Appends a negating AND clause to the query if the value is not null. /// @@ -231,6 +231,21 @@ public static class ParameterizedQueryBuilderExtensions return query; } + /// + /// Appends an ORDER BY clause to the query. + /// + /// The query. + /// The fields by which to order. + public static ParameterizedQuery OrderBy(this ParameterizedQuery query, params OrderField[] orderFields) + { + if (!orderFields.Any()) + return query; + + var clauses = string.Join(",", orderFields.Select(x => $"{x.Field} {(x.Direction == OrderDirection.Ascending ? "asc" : "desc")}")); + query.Sql.AppendLine($"order by {clauses}"); + return query; + } + /// /// Appends an OFFSET clause to the query. /// diff --git a/src/modules/Elsa.Dapper/Models/OrderField.cs b/src/modules/Elsa.Dapper/Models/OrderField.cs new file mode 100644 index 000000000..a7d87ef5c --- /dev/null +++ b/src/modules/Elsa.Dapper/Models/OrderField.cs @@ -0,0 +1,10 @@ +using Elsa.Common.Entities; + +namespace Elsa.Dapper.Models; + +/// +/// Represents a field by which to order. +/// +/// The field. +/// The direction. +public record OrderField(string Field, OrderDirection Direction); \ No newline at end of file diff --git a/src/modules/Elsa.Dapper/Modules/Runtime/Records/WorkflowStateRecord.cs b/src/modules/Elsa.Dapper/Modules/Runtime/Records/WorkflowStateRecord.cs index 53829689c..5c76bc647 100644 --- a/src/modules/Elsa.Dapper/Modules/Runtime/Records/WorkflowStateRecord.cs +++ b/src/modules/Elsa.Dapper/Modules/Runtime/Records/WorkflowStateRecord.cs @@ -8,5 +8,6 @@ internal class WorkflowStateRecord public string? CorrelationId { get; set; } public string Status { get; set; } = default!; public string SubStatus { get; set; } = default!; + public long ExecutionLogSequence { get; set; } public string Props { get; set; } = default!; } \ No newline at end of file diff --git a/src/modules/Elsa.Dapper/Services/Store.cs b/src/modules/Elsa.Dapper/Services/Store.cs index 5a24beeab..414532ab6 100644 --- a/src/modules/Elsa.Dapper/Services/Store.cs +++ b/src/modules/Elsa.Dapper/Services/Store.cs @@ -79,6 +79,19 @@ public class Store where T : notnull public async Task> FindManyAsync(Action filter, PageArgs pageArgs, string orderKey, OrderDirection orderDirection, CancellationToken cancellationToken = default) => await FindManyAsync(filter, pageArgs, orderKey, orderDirection, cancellationToken); + /// + /// Returns a page of records in the specified shape. + /// + /// The conditions to apply to the query. + /// The page arguments. + /// The fields by which to order the results. + /// The cancellation token. + /// A page of records. + public async Task> FindManyAsync(Action filter, PageArgs pageArgs, IEnumerable orderFields, CancellationToken cancellationToken = default) + { + return await FindManyAsync(filter, pageArgs, orderFields, cancellationToken); + } + /// /// Returns a page of records in the specified shape. /// @@ -90,12 +103,26 @@ public class Store where T : notnull /// The shape type. /// A page of records. public async Task> FindManyAsync(Action filter, PageArgs pageArgs, string orderKey, OrderDirection orderDirection, CancellationToken cancellationToken = default) + { + return await FindManyAsync(filter, pageArgs, new[] { new OrderField(orderKey, orderDirection) }, cancellationToken); + } + + /// + /// Returns a page of records in the specified shape. + /// + /// The conditions to apply to the query. + /// The page arguments. + /// The fields by which to order the results. + /// The cancellation token. + /// The shape type. + /// A page of records. + public async Task> FindManyAsync(Action filter, PageArgs pageArgs, IEnumerable orderFields, CancellationToken cancellationToken = default) { using var connection = _dbConnectionProvider.GetConnection(); var query = _dbConnectionProvider.CreateQuery().From(TableName); filter(query); - query = query.OrderBy(orderKey, orderDirection).Page(pageArgs); + query = query.OrderBy(orderFields.ToArray()).Page(pageArgs); var countQuery = _dbConnectionProvider.CreateQuery().Count(TableName); filter(countQuery); diff --git a/src/modules/Elsa.Elasticsearch/Modules/Runtime/WorkflowExecutionLogStore.cs b/src/modules/Elsa.Elasticsearch/Modules/Runtime/WorkflowExecutionLogStore.cs index 95aa56b6d..fe568a6d6 100644 --- a/src/modules/Elsa.Elasticsearch/Modules/Runtime/WorkflowExecutionLogStore.cs +++ b/src/modules/Elsa.Elasticsearch/Modules/Runtime/WorkflowExecutionLogStore.cs @@ -3,6 +3,7 @@ using Elastic.Clients.Elasticsearch.QueryDsl; using Elsa.Common.Entities; using Elsa.Common.Models; using Elsa.Elasticsearch.Common; +using Elsa.Elasticsearch.Shared.Models; using Elsa.Extensions; using Elsa.Workflows.Runtime.Contracts; using Elsa.Workflows.Runtime.Entities; @@ -64,11 +65,21 @@ public class ElasticWorkflowExecutionLogStore : IWorkflowExecutionLogStore private static SearchRequestDescriptor Sort(SearchRequestDescriptor descriptor, WorkflowExecutionLogRecordOrder order) { - var sortDescriptor = new SortOptionsDescriptor(); var propName = order.KeySelector.GetProperty()!.Name; - var sortOrder = order.Direction == OrderDirection.Ascending ? SortOrder.Asc : SortOrder.Desc; - sortDescriptor.Field(propName, f => f.Order(sortOrder)); - + var orderField = new OrderField(propName, order.Direction); + return Sort(descriptor, orderField); + } + + private static SearchRequestDescriptor Sort(SearchRequestDescriptor descriptor, params OrderField[] orderFields) + { + var sortDescriptor = new SortOptionsDescriptor(); + + foreach (var orderField in orderFields) + { + var sortOrder = orderField.Direction == OrderDirection.Ascending ? SortOrder.Asc : SortOrder.Desc; + sortDescriptor.Field(orderField.Field, f => f.Order(sortOrder)); + } + descriptor.Sort(sortDescriptor); return descriptor; } diff --git a/src/modules/Elsa.Elasticsearch/Shared/Models/OrderField.cs b/src/modules/Elsa.Elasticsearch/Shared/Models/OrderField.cs new file mode 100644 index 000000000..9de28785f --- /dev/null +++ b/src/modules/Elsa.Elasticsearch/Shared/Models/OrderField.cs @@ -0,0 +1,10 @@ +using Elsa.Common.Entities; + +namespace Elsa.Elasticsearch.Shared.Models; + +/// +/// Represents a field by which to order. +/// +/// The field. +/// The direction. +public record OrderField(string Field, OrderDirection Direction); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Models.cs index 5929df37e..f34900a19 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Models.cs @@ -19,7 +19,11 @@ internal class Response public WorkflowSubStatus SubStatus { get; set; } public string? CorrelationId { get; set; } public string? Name { get; set; } - public IDictionary Properties { get; set; } + + [Obsolete("Use WorkflowState.Properties instead.")] + public IDictionary Properties { get; set; } = new Dictionary(); + + [Obsolete("Use WorkflowState.Fault instead.")] public WorkflowFaultState? Fault { get; set; } public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset? LastExecutedAt { get; set; } diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowTriggerExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowTriggerExtensions.cs index 4ae078fec..202eb110a 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowTriggerExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowTriggerExtensions.cs @@ -5,8 +5,16 @@ using Elsa.Workflows.Runtime.Entities; // ReSharper disable once CheckNamespace namespace Elsa.Extensions; +/// +/// Extension methods for . +/// public static class WorkflowTriggerExtensions { + /// + /// Filters a collection of by trigger type. + /// + /// The collection of triggers to filter. + /// The trigger type. public static IEnumerable Filter(this IEnumerable triggers) where T : ITrigger { var triggerName = ActivityTypeNameHelper.GenerateTypeName();