diff --git a/Elsa.sln b/Elsa.sln index 3bd4eac22..34eb44ccf 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -403,6 +403,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "oracle-setup", "oracle-setu docker\oracle-setup\setup.sql = docker\oracle-setup\setup.sql EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "connections", "connections", "{9DF81143-2F39-4CFB-9DA3-902279010622}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Connections.Models", "src\modules\Elsa.Connections.Models\Elsa.Connections.Models.csproj", "{75E08B0E-A41A-425C-9DCA-155505CE41CD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Connections.Api", "src\modules\Elsa.Connections.Api\Elsa.Connections.Api.csproj", "{BD120D97-4862-469E-B2CF-4109F7F21C20}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Connections.Core", "src\modules\Elsa.Connections.Core\Elsa.Connections.Core.csproj", "{8A3443F3-D40E-44B9-9805-B4CB7A3F022C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Connections.Persistence", "src\modules\Elsa.Connections.Persistence\Elsa.Connections.Persistence.csproj", "{FA2B2B44-7E3E-444B-A157-7526B376E9CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Connections.Persistence.EntityFrameworkCore", "src\modules\Elsa.Connections.Persistence.EntityFrameworkCore\Elsa.Connections.Persistence.EntityFrameworkCore.csproj", "{DF0CA7E2-F5DF-40ED-91B6-748767084FC7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite", "src\modules\Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite\Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj", "{DB20B168-CD90-41F2-BE08-3D8C6F142459}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -859,6 +873,30 @@ Global {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Debug|Any CPU.Build.0 = Debug|Any CPU {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Release|Any CPU.ActiveCfg = Release|Any CPU {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Release|Any CPU.Build.0 = Release|Any CPU + {75E08B0E-A41A-425C-9DCA-155505CE41CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75E08B0E-A41A-425C-9DCA-155505CE41CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75E08B0E-A41A-425C-9DCA-155505CE41CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75E08B0E-A41A-425C-9DCA-155505CE41CD}.Release|Any CPU.Build.0 = Release|Any CPU + {BD120D97-4862-469E-B2CF-4109F7F21C20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD120D97-4862-469E-B2CF-4109F7F21C20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD120D97-4862-469E-B2CF-4109F7F21C20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD120D97-4862-469E-B2CF-4109F7F21C20}.Release|Any CPU.Build.0 = Release|Any CPU + {8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A3443F3-D40E-44B9-9805-B4CB7A3F022C}.Release|Any CPU.Build.0 = Release|Any CPU + {FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA2B2B44-7E3E-444B-A157-7526B376E9CA}.Release|Any CPU.Build.0 = Release|Any CPU + {DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF0CA7E2-F5DF-40ED-91B6-748767084FC7}.Release|Any CPU.Build.0 = Release|Any CPU + {DB20B168-CD90-41F2-BE08-3D8C6F142459}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB20B168-CD90-41F2-BE08-3D8C6F142459}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB20B168-CD90-41F2-BE08-3D8C6F142459}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB20B168-CD90-41F2-BE08-3D8C6F142459}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1011,6 +1049,13 @@ Global {FA5E857F-B173-4B5D-8049-B817A210DEF5} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} {A51F9683-DA9F-45E7-82DE-1E261ACD6D68} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} {66E2E2CF-967F-4564-89E8-F46FA973C99B} = {986E5482-0482-448C-B9E4-EC67A9474B85} + {9DF81143-2F39-4CFB-9DA3-902279010622} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} + {75E08B0E-A41A-425C-9DCA-155505CE41CD} = {9DF81143-2F39-4CFB-9DA3-902279010622} + {BD120D97-4862-469E-B2CF-4109F7F21C20} = {9DF81143-2F39-4CFB-9DA3-902279010622} + {8A3443F3-D40E-44B9-9805-B4CB7A3F022C} = {9DF81143-2F39-4CFB-9DA3-902279010622} + {FA2B2B44-7E3E-444B-A157-7526B376E9CA} = {9DF81143-2F39-4CFB-9DA3-902279010622} + {DF0CA7E2-F5DF-40ED-91B6-748767084FC7} = {9DF81143-2F39-4CFB-9DA3-902279010622} + {DB20B168-CD90-41F2-BE08-3D8C6F142459} = {9DF81143-2F39-4CFB-9DA3-902279010622} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/apps/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj b/src/apps/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj index 7cfa13de3..7df79071e 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj +++ b/src/apps/Elsa.ServerAndStudio.Web/Elsa.ServerAndStudio.Web.csproj @@ -13,6 +13,10 @@ + + + + diff --git a/src/apps/Elsa.ServerAndStudio.Web/Program.cs b/src/apps/Elsa.ServerAndStudio.Web/Program.cs index 6d4aa8895..807f711a0 100644 --- a/src/apps/Elsa.ServerAndStudio.Web/Program.cs +++ b/src/apps/Elsa.ServerAndStudio.Web/Program.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Proto.Persistence.Sqlite; using WebhooksCore.Options; +using Elsa.Connections.Middleware; const bool useMassTransit = true; const bool useProtoActor = true; @@ -193,6 +194,11 @@ services if (distributedCachingTransport == DistributedCachingTransport.MassTransit) distributedCaching.UseMassTransit(); }); } + + elsa.UseConnections( + configure=> configure.AddConnectionsFrom()); + elsa.UseConnectionPersistence(ef=> ef.UseEntityFrameworkCore(f=>f.UseSqlite())); + elsa.UseConnectionsApi(); }); services.Configure(options => configuration.GetSection("Webhooks").Bind(options)); diff --git a/src/modules/Elsa.Connections.Api/Constants.cs b/src/modules/Elsa.Connections.Api/Constants.cs new file mode 100644 index 000000000..732c2da99 --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Constants.cs @@ -0,0 +1,5 @@ +namespace Elsa.Connections.Api; +public class Constants +{ + public const string PermissionsNamespace = "connections"; +} diff --git a/src/modules/Elsa.Connections.Api/Elsa.Connections.Api.csproj b/src/modules/Elsa.Connections.Api/Elsa.Connections.Api.csproj new file mode 100644 index 000000000..fbf376330 --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Elsa.Connections.Api.csproj @@ -0,0 +1,14 @@ + + + + Provides Activity Connection API endpoints + elsa module activity connection abstraction api + + + + + + + + + diff --git a/src/modules/Elsa.Connections.Api/Endpoints/ActivityConnectionDescriptor/Get/Endpoint.cs b/src/modules/Elsa.Connections.Api/Endpoints/ActivityConnectionDescriptor/Get/Endpoint.cs new file mode 100644 index 000000000..7bf4aa6eb --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Endpoints/ActivityConnectionDescriptor/Get/Endpoint.cs @@ -0,0 +1,29 @@ +using Elsa.Abstractions; +using Elsa.Connections.Contracts; +using Elsa.Workflows.Models; + +namespace Elsa.Connections.Api.Endpoints.ActivityConnectionDescriptor.Get; + +public class Endpoint(IConnectionDescriptorRegistry store) : ElsaEndpointWithoutRequest> +{ + public override void Configure() + { + Get("/connection-configuration/input-descriptor/{ActivityType}"); + ConfigurePermissions($"{Constants.PermissionsNamespace}/descriptor:read"); + } + + public override async Task HandleAsync(CancellationToken ct) + { + var type = Route("ActivityType"); + + if (string.IsNullOrWhiteSpace(type)) + { + AddError("ActivityType is required"); + await SendErrorsAsync(cancellation: ct); + return; + } + + var config = await store.GetConnectionDescriptorAsync(type, ct); + await SendOkAsync(config, ct); + } +} diff --git a/src/modules/Elsa.Connections.Api/Endpoints/ActivityConnectionDescriptor/List/Endpoint.cs b/src/modules/Elsa.Connections.Api/Endpoints/ActivityConnectionDescriptor/List/Endpoint.cs new file mode 100644 index 000000000..62e35582b --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Endpoints/ActivityConnectionDescriptor/List/Endpoint.cs @@ -0,0 +1,22 @@ +using Elsa.Abstractions; +using Elsa.Common.Models; +using Elsa.Connections.Contracts; +using Elsa.Connections.Models; +using Elsa.Models; + +namespace Elsa.Connections.Api.Endpoints.ActivityConnectionDescriptor.List; + +public class List(IConnectionDescriptorRegistry registry) : ElsaEndpointWithoutRequest> +{ + public override void Configure() + { + Get("/connection-configuration/descriptors"); + ConfigurePermissions($"{Constants.PermissionsNamespace}/descriptor:read"); + } + + public override Task> ExecuteAsync(CancellationToken ct) + { + var descriptors = registry.ListAll().ToList(); + return Task.FromResult(new PagedListResponse(Page.Of(descriptors, descriptors.Count)) ); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Api/Endpoints/Add/Endpoint.cs b/src/modules/Elsa.Connections.Api/Endpoints/Add/Endpoint.cs new file mode 100644 index 000000000..8f7cf39c5 --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Endpoints/Add/Endpoint.cs @@ -0,0 +1,53 @@ +using Elsa.Abstractions; +using Elsa.Connections.Models; +using Elsa.Connections.Persistence.Contracts; +using Elsa.Connections.Persistence.Filters; +using Elsa.Workflows; +using Elsa.Connections.Persistence.Entities; +using Elsa.Connections.Api.Extensions; + +namespace Elsa.Connections.Api.Endpoints.Add; + +public class Endpoint(IConnectionStore store, IIdentityGenerator identityGenerator) : ElsaEndpoint +{ + public override void Configure() + { + Post("/connection-configuration"); + ConfigurePermissions($"{Constants.PermissionsNamespace}:write"); + } + + public override async Task ExecuteAsync(ConnectionInputModel model, CancellationToken ct) + { + var isNameUnique = await IsNameUniqueAsync(model.Name, ct); + + if (!isNameUnique) + { + AddError("A connection already exists with the specified name"); + await SendErrorsAsync(cancellation: ct); + return null!; + } + + var newEntity = new ConnectionDefinition + { + Id = identityGenerator.GenerateId(), + Name = model.Name, + Description = model.Description, + ConnectionType = model.ConnectionType, + ConnectionConfiguration = model.ConnectionConfiguration, + }; + await store.AddAsync(newEntity, ct); + await SendOkAsync(ct); + + return newEntity.ToModel(); + } + + private async Task IsNameUniqueAsync(string name, CancellationToken ct) + { + var filter = new ConnectionDefinitionFilter + { + Name = name + }; + return await store.FindAsync(filter, ct) == null; + } +} + diff --git a/src/modules/Elsa.Connections.Api/Endpoints/Delete/Endpoint.cs b/src/modules/Elsa.Connections.Api/Endpoints/Delete/Endpoint.cs new file mode 100644 index 000000000..d8fda228e --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Endpoints/Delete/Endpoint.cs @@ -0,0 +1,27 @@ +using Elsa.Abstractions; +using Elsa.Connections.Persistence.Contracts; + +namespace Elsa.Connections.Api.Endpoints.Delete; + +public class Endpoint(IConnectionStore store) : ElsaEndpoint +{ + public override void Configure() + { + Delete("/connection-configuration/{id}"); + ConfigurePermissions($"{Constants.PermissionsNamespace}:delete"); + } + + public override async Task HandleAsync(Request req, CancellationToken ct) + { + var entity = await store.GetAsync(req.Id, ct); + + if (entity == null) + { + await SendNotFoundAsync(ct); + return; + } + + await store.DeleteAsync(entity, ct); + await SendOkAsync(ct); + } +} diff --git a/src/modules/Elsa.Connections.Api/Endpoints/Delete/Request.cs b/src/modules/Elsa.Connections.Api/Endpoints/Delete/Request.cs new file mode 100644 index 000000000..6e33ffacf --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Endpoints/Delete/Request.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Connections.Api.Endpoints.Delete; + +public class Request +{ + [Required] public string Id { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Api/Endpoints/Get/Endpoint.cs b/src/modules/Elsa.Connections.Api/Endpoints/Get/Endpoint.cs new file mode 100644 index 000000000..bbf6ab953 --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Endpoints/Get/Endpoint.cs @@ -0,0 +1,32 @@ +using Elsa.Abstractions; +using Elsa.Connections.Models; +using Elsa.Connections.Persistence.Contracts; +using Elsa.Connections.Api.Extensions; + +namespace Elsa.Connections.Api.Endpoints.Get; + +public class Endpoint(IConnectionStore store) : ElsaEndpoint +{ + public override void Configure() + { + Get("/connection-configuration/{id}"); + ConfigurePermissions($"{Constants.PermissionsNamespace}:read"); + } + + public override async Task ExecuteAsync(Request req, CancellationToken ct) + { + var entity = await store.FindAsync( + new() + { + Id = req.Id + }, ct); + + if (entity == null) + { + await SendNotFoundAsync(ct); + return null!; + } + + return entity.ToModel(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Api/Endpoints/Get/Request.cs b/src/modules/Elsa.Connections.Api/Endpoints/Get/Request.cs new file mode 100644 index 000000000..933b0c2fb --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Endpoints/Get/Request.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Connections.Api.Endpoints.Get; + +public class Request +{ + [Required] public string Id { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Api/Endpoints/List/Endpoint.cs b/src/modules/Elsa.Connections.Api/Endpoints/List/Endpoint.cs new file mode 100644 index 000000000..9f791b69e --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Endpoints/List/Endpoint.cs @@ -0,0 +1,24 @@ +using Elsa.Abstractions; +using Elsa.Common.Models; +using Elsa.Connections.Api.Extensions; +using Elsa.Connections.Models; +using Elsa.Connections.Persistence.Contracts; +using Elsa.Models; + +namespace Elsa.Connections.Api.Endpoints.List; + +public class Endpoint(IConnectionStore store) : ElsaEndpointWithoutRequest> +{ + public override void Configure() + { + Get("/connection-configuration"); + ConfigurePermissions($"{Constants.PermissionsNamespace}:read"); + } + + public override async Task> ExecuteAsync(CancellationToken ct) + { + var entities = await store.ListAsync(ct); + var models = entities.Select(x => x.ToModel()).ToList(); + return new(Page.Of(models, models.Count())); + } +} diff --git a/src/modules/Elsa.Connections.Api/Endpoints/Update/Endpoint.cs b/src/modules/Elsa.Connections.Api/Endpoints/Update/Endpoint.cs new file mode 100644 index 000000000..ffba22422 --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Endpoints/Update/Endpoint.cs @@ -0,0 +1,55 @@ +using Elsa.Abstractions; +using Elsa.Connections.Models; +using Elsa.Connections.Persistence.Contracts; +using Elsa.Connections.Persistence.Filters; +using Elsa.Connections.Api.Extensions; + +namespace Elsa.Connections.Api.Endpoints.Update; + +public class Endpoint(IConnectionStore store) : ElsaEndpoint +{ + public override void Configure() + { + Put("/connection-configuration/{id}"); + ConfigurePermissions($"{Constants.PermissionsNamespace}:write"); + } + + public override async Task ExecuteAsync(ConnectionInputModel model, CancellationToken ct) + { + var id = Route("id")!; + var entity = await store.GetAsync(id, ct); + + if (entity == null) + { + await SendNotFoundAsync(ct); + return null!; + } + + var isNameDuplicate = await IsNameDuplicateAsync(model.Name, id, ct); + + if (isNameDuplicate) + { + AddError("Another connection already exist with the specified name"); + await SendErrorsAsync(cancellation: ct); + return entity.ToModel(); + } + + entity.Name = model.Name; + entity.Description = model.Description; + entity.ConnectionConfiguration = model.ConnectionConfiguration; + + await store.UpdateAsync(entity, ct); + return entity.ToModel(); + } + + private async Task IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken) + { + var entities = await store.FindAsync(new() + { + NotId = id, + Name = name + }, cancellationToken); + + return entities != null; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Api/Extensions/ConnectionDefinitionExtensions.cs b/src/modules/Elsa.Connections.Api/Extensions/ConnectionDefinitionExtensions.cs new file mode 100644 index 000000000..fef90c617 --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Extensions/ConnectionDefinitionExtensions.cs @@ -0,0 +1,19 @@ +using Elsa.Connections.Models; +using Elsa.Connections.Persistence.Entities; + +namespace Elsa.Connections.Api.Extensions; + +public static class ConnectionDefinitionExtensions +{ + public static ConnectionModel ToModel(this ConnectionDefinition entity) + { + return new() + { + Id = entity.Id, + Name = entity.Name, + Description = entity.Description, + ConnectionType = entity.ConnectionType, + ConnectionConfiguration = entity.ConnectionConfiguration, + }; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Api/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Connections.Api/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..10cc54185 --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Extensions/ModuleExtensions.cs @@ -0,0 +1,19 @@ +using Elsa.Connections.Api.Features; +using Elsa.Features.Services; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Extends with methods to Connection API endpoints. +/// +public static class ModuleExtensions +{ + /// + /// Installs the Connections API feature. + /// + public static IModule UseConnectionsApi(this IModule module, Action? configure = null) + { + return module.Use(configure); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Api/Features/ConnectionsApiFeature.cs b/src/modules/Elsa.Connections.Api/Features/ConnectionsApiFeature.cs new file mode 100644 index 000000000..d3fc3c7a4 --- /dev/null +++ b/src/modules/Elsa.Connections.Api/Features/ConnectionsApiFeature.cs @@ -0,0 +1,22 @@ +using Elsa.Connections.Persistence.Features; +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using JetBrains.Annotations; + +namespace Elsa.Connections.Api.Features; + +/// +/// A feature that installs API endpoints to interact connections. +/// +[DependsOn(typeof(ConnectionPersistenceFeature))] +[UsedImplicitly] +public class ConnectionsApiFeature(IModule module) : FeatureBase(module) +{ + /// + public override void Configure() + { + Module.AddFastEndpointsAssembly(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Api/FodyWeavers.xml b/src/modules/Elsa.Connections.Api/FodyWeavers.xml new file mode 100644 index 000000000..ac6b5b292 --- /dev/null +++ b/src/modules/Elsa.Connections.Api/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Core/Attributes/ConnectionActivityAttribute.cs b/src/modules/Elsa.Connections.Core/Attributes/ConnectionActivityAttribute.cs new file mode 100644 index 000000000..9156be919 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Attributes/ConnectionActivityAttribute.cs @@ -0,0 +1,10 @@ +namespace Elsa.Connections.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public class ConnectionActivityAttribute(string type) : Attribute +{ + /// + /// The TypeName of the connection + /// + public string Type { get; set; } = type; +} diff --git a/src/modules/Elsa.Connections.Core/Attributes/ConnectionPropertyAttribute.cs b/src/modules/Elsa.Connections.Core/Attributes/ConnectionPropertyAttribute.cs new file mode 100644 index 000000000..98c6d79c3 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Attributes/ConnectionPropertyAttribute.cs @@ -0,0 +1,8 @@ +namespace Elsa.Connections.Attributes; + +public class ConnectionPropertyAttribute(string ns, string displayName, string? description = null) : Attribute +{ + public string? Namespace { get; set; } = ns; + public string? Description { get; set; } = description; + public string? DisplayName { get; set; } = displayName; +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Core/Attributes/ConnectionTypeAttribute.cs b/src/modules/Elsa.Connections.Core/Attributes/ConnectionTypeAttribute.cs new file mode 100644 index 000000000..e000eb742 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Attributes/ConnectionTypeAttribute.cs @@ -0,0 +1,13 @@ +namespace Elsa.Connections.Attributes; + +public class ConnectionTypeAttribute : Attribute +{ + public ConnectionTypeAttribute(Type type) + { + Type = type; + } + /// + /// The TypeName of the connection + /// + public Type Type { get; set; } +} diff --git a/src/modules/Elsa.Connections.Core/Attributes/NoLogAttribute.cs b/src/modules/Elsa.Connections.Core/Attributes/NoLogAttribute.cs new file mode 100644 index 000000000..80c4275c9 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Attributes/NoLogAttribute.cs @@ -0,0 +1,5 @@ +namespace Elsa.Connections.Attributes; + +public class NoLogAttribute : Attribute +{ +} diff --git a/src/modules/Elsa.Connections.Core/Contracts/IConnectionDescriptorRegistry.cs b/src/modules/Elsa.Connections.Core/Contracts/IConnectionDescriptorRegistry.cs new file mode 100644 index 000000000..61e525987 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Contracts/IConnectionDescriptorRegistry.cs @@ -0,0 +1,40 @@ +using Elsa.Connections.Models; +using Elsa.Workflows.Models; + +namespace Elsa.Connections.Contracts; + +/// +/// Store all connection descriptors available to the system +/// +public interface IConnectionDescriptorRegistry +{ + /// + /// Adds a connection descriptor to the registry + /// + /// The type of the connection + /// The type desiptor of the connection + void Add(Type connectionType, ConnectionDescriptor connectionDescriptor); + + /// + /// Removes an activity descriptor from the registry. + /// + /// The type of the connection. + /// + void Remove(Type connectionType, ActivityDescriptor descriptor); + + /// + /// Returns all connection descriptors in the registry. + /// + /// All connection descriptors in the registry. + IEnumerable ListAll(); + + Type? Get(string type); + + /// + /// Get the Input Descriptors for an connection + /// + /// The type of the connectn + /// + /// + Task> GetConnectionDescriptorAsync(string activityType, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Core/Contracts/IConnectionDescriptorRepository.cs b/src/modules/Elsa.Connections.Core/Contracts/IConnectionDescriptorRepository.cs new file mode 100644 index 000000000..ffb11ba74 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Contracts/IConnectionDescriptorRepository.cs @@ -0,0 +1,6 @@ +namespace Elsa.Connections.Contracts; + +public interface IConnectionDescriptorRegistryEX +{ + +} diff --git a/src/modules/Elsa.Connections.Core/Contracts/IConnectionProperty.cs b/src/modules/Elsa.Connections.Core/Contracts/IConnectionProperty.cs new file mode 100644 index 000000000..fd89db4df --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Contracts/IConnectionProperty.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace Elsa.Connections.Contracts; +interface IConnectionProperty +{ + public string ConnectionName { get; set; } + + [JsonIgnore] + public object Properties { get; set; } +} + diff --git a/src/modules/Elsa.Connections.Core/Converters/Int32Converter.cs b/src/modules/Elsa.Connections.Core/Converters/Int32Converter.cs new file mode 100644 index 000000000..adb992078 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Converters/Int32Converter.cs @@ -0,0 +1,29 @@ +using System.Text.Json; + +namespace Elsa.Connections.Converters; + +public class Int32Converter : System.Text.Json.Serialization.JsonConverter +{ + public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + var stringValue = reader.GetString(); + if (int.TryParse(stringValue, out var value)) + { + return value; + } + } + else if (reader.TokenType == JsonTokenType.Number) + { + return reader.GetInt32(); + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value); + } +} diff --git a/src/modules/Elsa.Connections.Core/Elsa.Connections.Core.csproj b/src/modules/Elsa.Connections.Core/Elsa.Connections.Core.csproj new file mode 100644 index 000000000..b92c7e94f --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Elsa.Connections.Core.csproj @@ -0,0 +1,17 @@ + + + + Provides an connection framework to use connection abtraction in activities + elsa module connection abstratction + Elsa.Connections + + + + + + + + + + + diff --git a/src/modules/Elsa.Connections.Core/Extensions/LogConnectionExtensions.cs b/src/modules/Elsa.Connections.Core/Extensions/LogConnectionExtensions.cs new file mode 100644 index 000000000..a11192cb8 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Extensions/LogConnectionExtensions.cs @@ -0,0 +1,24 @@ +// ReSharper disable once CheckNamespace + +using Microsoft.Extensions.Logging; + +namespace Elsa.Connections.Core.Extensions; + +/// +/// +/// +public static partial class LogConnectionExtensions +{ + [LoggerMessage( + Level = LogLevel.Warning, + Message = "Connection {name} was not found" + )] + public static partial void LogConnectionNotFound(ILogger logger, string name); + + [LoggerMessage( + Level = LogLevel.Warning, + Message = "Connection is null" + )] + public static partial void LogConnectionIsNull(ILogger logger); + +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Core/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Connections.Core/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..d68959b7a --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Extensions/ModuleExtensions.cs @@ -0,0 +1,33 @@ +using Elsa.Connections.Features; +using Elsa.Connections.Middleware; +using Elsa.Features.Services; +using Elsa.Workflows; +using Elsa.Workflows.Pipelines.ActivityExecution; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Extends with methods to install Semantic Kernel API endpoints. +/// +public static class ModuleExtensions +{ + /// + /// Installs the Semantic Kernel API feature. + /// + public static IModule UseConnections(this IModule module, Action? configure = null) + { + return module.Use(configure); + } +} + +/// +/// Adds extension methods to . +/// +public static class ConnectionMiddlewareExtensions +{ + /// + /// Installs the component in the activity execution pipeline. + /// + public static IActivityExecutionPipelineBuilder UseConnectionMiddleware(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware(); +} diff --git a/src/modules/Elsa.Connections.Core/Features/ConnectionsFeatures.cs b/src/modules/Elsa.Connections.Core/Features/ConnectionsFeatures.cs new file mode 100644 index 000000000..8fc227e7a --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Features/ConnectionsFeatures.cs @@ -0,0 +1,73 @@ +using System.Reflection; +using Elsa.Connections.Attributes; +using Elsa.Connections.Contracts; +using Elsa.Connections.Filters; +using Elsa.Connections.ServiceProvider; +using Elsa.Connections.Services; +using Elsa.Connections.UIHints; +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Elsa.Workflows; +using Elsa.Workflows.Pipelines.ActivityExecution; +using Elsa.Workflows.Features; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Elsa.Connections.Middleware; +using Elsa.Connections.Models; + +namespace Elsa.Connections.Features; + +/// +/// A feature that installs API endpoints to interact with skilled agents. +/// +[DependsOn(typeof(WorkflowsFeature))] +[UsedImplicitly] +public class ConnectionsFeatures(IModule module) : FeatureBase(module) +{ + + /// + /// A set of connection types to make available to the system. + /// + public HashSet ConnectionTypes { get; } = []; + + /// + public override void Apply() + { + // Obfuscate Connection Properties. + Services.AddActivityStateFilter(); + // Activity property options providers. + Services.AddScoped(); + + Services.AddSingleton(); + + //UIHints + Services.AddScoped(); + + Services.Configure(options => + { + foreach (var connectionType in ConnectionTypes.Distinct()) + options.ConnectionTypes.Add(connectionType); + }); + } + public override void Configure() + { + //TODO: Need to insert the middleware just before the BackgroundActivityInvoker + //How to be sure that it is inserted before? + var workflowFeature = Module.Configure() + .WithDefaultActivityExecutionPipeline(pipeline => pipeline.Insert(3)); + base.Configure(); + } + public ConnectionsFeatures AddConnectionsFrom() + { + var connectionTypes = typeof(TMarker).Assembly.GetExportedTypes() + .Where(x => x.GetCustomAttribute() != null ) + .ToList(); + + ConnectionTypes.AddRange(connectionTypes); + + return this; + } + +} diff --git a/src/modules/Elsa.Connections.Core/Filters/PropertyAttributeFilter.cs b/src/modules/Elsa.Connections.Core/Filters/PropertyAttributeFilter.cs new file mode 100644 index 000000000..14773afa7 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Filters/PropertyAttributeFilter.cs @@ -0,0 +1,25 @@ +using Elsa.Connections.Attributes; +using Elsa.Workflows; + +namespace Elsa.Connections.Filters; + +public class PropertyAttributeFilter : ActivityStateFilterBase +{ + protected override ActivityStateFilterResult OnExecute(ActivityStateFilterContext context) + { + var inputDescriptor = context.InputDescriptor; + + if (Attribute.IsDefined(inputDescriptor.PropertyInfo, typeof(NoLogAttribute))) + { + var contextValue = context.Value.GetProperty("connectionName").GetString(); + + if (contextValue == null) + return ActivityStateFilterResult.Pass(); + + var maskedValue = $"**** see connection information for {contextValue} ****"; + return Filtered(maskedValue); + } + + return ActivityStateFilterResult.Pass(); + } +} diff --git a/src/modules/Elsa.Connections.Core/FodyWeavers.xml b/src/modules/Elsa.Connections.Core/FodyWeavers.xml new file mode 100644 index 000000000..ac6b5b292 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Core/Middleware/ConnectionMiddleware.cs b/src/modules/Elsa.Connections.Core/Middleware/ConnectionMiddleware.cs new file mode 100644 index 000000000..736470521 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Middleware/ConnectionMiddleware.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using Elsa.Connections.Attributes; +using Elsa.Connections.Models; +using Elsa.Extensions; +using Elsa.Workflows; +using Elsa.Workflows.Pipelines.ActivityExecution; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Elsa.Connections.Core.Extensions; +using Elsa.Connections.Converters; +using Elsa.Connections.Persistence.Contracts; + +namespace Elsa.Connections.Middleware; + +/// +/// An activity execution middleware component that injects connection properties from Connection Name. +/// +[UsedImplicitly] +public class ConnectionMiddleware(ActivityMiddlewareDelegate next + , IConnectionStore connectionStore + ,ILogger logger) + : IActivityExecutionMiddleware +{ + private static JsonSerializerOptions? _serializerOptions; + + private static JsonSerializerOptions SerializerOptions => + _serializerOptions ??= new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }.WithConverters(new Int32Converter()); + + /// + public async ValueTask InvokeAsync(ActivityExecutionContext context) + { + var activityDescriptor = context.ActivityDescriptor; + + if (activityDescriptor.Attributes.Any(attr => attr.GetType() == typeof(ConnectionActivityAttribute))) + { + var inputDescriptors = activityDescriptor.Inputs.Where(x => x.PropertyInfo?.PropertyType.GetGenericTypeDefinition() == typeof(ConnectionProperties<>)).ToList(); + + if (inputDescriptors.Count > 0) + { + var input = inputDescriptors[0]; + var propertyType = input.PropertyInfo?.PropertyType.GetGenericArguments()[0]; + + dynamic inputValue = input.ValueGetter(context.Activity)!; + var connectionName = (string)inputValue.ConnectionName; + + if (connectionName == null) + LogConnectionExtensions.LogConnectionIsNull(logger); + else + { + //Get connection from store, if exist, + var connectionConfiguration = await connectionStore.FindAsync(new Persistence.Filters.ConnectionDefinitionFilter() { Name = connectionName }); + if (connectionConfiguration != null) + { + dynamic deserializedjson = JsonSerializer.Deserialize(connectionConfiguration.ConnectionConfiguration, propertyType, SerializerOptions); + + inputValue.Properties = deserializedjson; + + input.ValueSetter(context.Activity, inputValue); + } + else + LogConnectionExtensions.LogConnectionNotFound(logger, connectionName); + } + } + } + + await next(context); + } +} diff --git a/src/modules/Elsa.Connections.Core/Models/ConnectionOptions.cs b/src/modules/Elsa.Connections.Core/Models/ConnectionOptions.cs new file mode 100644 index 000000000..bf9dc94d3 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Models/ConnectionOptions.cs @@ -0,0 +1,9 @@ +namespace Elsa.Connections.Models; + +public class ConnectionOptions +{ + /// + /// A collection of connection types that are available to the system. + /// + public HashSet ConnectionTypes { get; set; } = new(); +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Core/ServiceProvider/ConnectionOptionsProvider.cs b/src/modules/Elsa.Connections.Core/ServiceProvider/ConnectionOptionsProvider.cs new file mode 100644 index 000000000..71657877e --- /dev/null +++ b/src/modules/Elsa.Connections.Core/ServiceProvider/ConnectionOptionsProvider.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using Elsa.Connections.Attributes; +using Elsa.Connections.Persistence.Contracts; +using Elsa.Workflows.UIHints.Dropdown; + +namespace Elsa.Connections.ServiceProvider; + +public class ConnectionOptionsProvider(IConnectionStore store) : DropDownOptionsProviderBase +{ + public new async ValueTask> GetUIPropertiesAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken = default) + { + var options = await base.GetUIPropertiesAsync(propertyInfo, context, cancellationToken); + options.Add("Refresh", true); + return options; + } + + protected override async ValueTask> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken) + { + var connection = new List(); + var connectionType = propertyInfo.GetCustomAttribute()?.Type; + + if (connectionType == null) + return connection; + + var connections = await store.FindManyAsync(new() { Type = connectionType.ToString() }, cancellationToken); + + foreach(var conn in connections) + connection.Add(new(conn.Name, conn.Name)); + + return connection; + } + + protected override IDictionary GetUIPropertyAdditionalOptions() + { + return new Dictionary + { + ["Refresh"] = true + }; + } +} diff --git a/src/modules/Elsa.Connections.Core/Services/ConnectionRegistry.cs b/src/modules/Elsa.Connections.Core/Services/ConnectionRegistry.cs new file mode 100644 index 000000000..6c94e1b59 --- /dev/null +++ b/src/modules/Elsa.Connections.Core/Services/ConnectionRegistry.cs @@ -0,0 +1,72 @@ +using System.Collections.Concurrent; +using System.Reflection; +using Elsa.Connections.Attributes; +using Elsa.Connections.Contracts; +using Elsa.Connections.Models; +using Elsa.Workflows; +using Elsa.Workflows.Models; +using Microsoft.Extensions.Options; + +namespace Elsa.Connections.Services; + +public class ConnectionRegistry(IOptions options, IActivityDescriber describer) : IConnectionDescriptorRegistry +{ + private readonly ConcurrentDictionary _connectionDescriptors = new(); + private readonly ConnectionOptions _options = options.Value; + + public void Add(Type connectionType, ConnectionDescriptor connectionDescriptor) + { + var descriptor = DescribeConnection(connectionType); + + _connectionDescriptors.TryAdd(connectionType, descriptor); + } + + public IEnumerable ListAll() + { + foreach (var connectionType in _options.ConnectionTypes) + yield return DescribeConnection(connectionType); + } + + public void Remove(Type connectionType, ActivityDescriptor descriptor) + { + throw new NotImplementedException(); + } + + public Type? Get(string type) + { + return _options.ConnectionTypes.FirstOrDefault(c => c.ToString() == type); + } + + public async Task> GetConnectionDescriptorAsync(string activityType, CancellationToken cancellationToken = default) + { + var propertyType = Get(activityType); + + if (propertyType == null) + return []; + + var connectionInputProperty = describer.GetInputProperties(propertyType); + var connectionInputDescriptor = await DescribeInputPropertiesAsync(connectionInputProperty, cancellationToken); + + return connectionInputDescriptor; + } + + private async Task> DescribeInputPropertiesAsync(IEnumerable properties, CancellationToken cancellationToken = default) + { + return await Task.WhenAll(properties.Select(async x => await describer.DescribeInputPropertyAsync(x, cancellationToken))); + } + + private ConnectionDescriptor DescribeConnection(Type connectionType) + { + var connectionAttribute = connectionType.GetCustomAttribute(); + + if (connectionAttribute == null) + throw new ArgumentNullException($"{connectionType} is not a valid connection, make sure [ConnectionPropertyAttribute] is set "); + + return new( + connectionType.ToString(), + connectionAttribute.Description, + connectionAttribute.Namespace, + connectionAttribute.DisplayName + ); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Core/UIHints/ConnectionDropDownUIHintHandler.cs b/src/modules/Elsa.Connections.Core/UIHints/ConnectionDropDownUIHintHandler.cs new file mode 100644 index 000000000..060ad752b --- /dev/null +++ b/src/modules/Elsa.Connections.Core/UIHints/ConnectionDropDownUIHintHandler.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using Elsa.Workflows; +using Elsa.Workflows.UIHints; +using Elsa.Workflows.UIHints.Dropdown; + +namespace Elsa.Connections.UIHints; + +/// +/// UIHint Handler that use DropDown for Connection Property +/// +public class ConnectionDropDownUIHintHandler : IUIHintHandler +{ + /// + public string UIHint => $"connection-{InputUIHints.DropDown}"; + + /// + public ValueTask> GetPropertyUIHandlersAsync(PropertyInfo propertyInfo, CancellationToken cancellationToken) + { + return new([typeof(StaticDropDownOptionsProvider)]); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Models/ConnectionDescriptor.cs b/src/modules/Elsa.Connections.Models/ConnectionDescriptor.cs new file mode 100644 index 000000000..b79c1a516 --- /dev/null +++ b/src/modules/Elsa.Connections.Models/ConnectionDescriptor.cs @@ -0,0 +1,3 @@ +namespace Elsa.Connections.Models; + +public record ConnectionDescriptor(string Type, string? Description, string? Namespace, string? ProviderName); diff --git a/src/modules/Elsa.Connections.Models/ConnectionInputModel.cs b/src/modules/Elsa.Connections.Models/ConnectionInputModel.cs new file mode 100644 index 000000000..497cdb1a8 --- /dev/null +++ b/src/modules/Elsa.Connections.Models/ConnectionInputModel.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Nodes; + +namespace Elsa.Connections.Models; + +public class ConnectionInputModel +{ + /// + /// The Name of the Connection + /// + [Required] public string Name { get; set; } = null!; + + /// + /// The Description of the Connection + /// + public string? Description { get; set; } + + /// + /// The Configuration of the Connection in a Json Format + /// + public JsonObject ConnectionConfiguration { get; set; } = new(); + + /// + /// The Connection Type Name + /// + [Required] public string ConnectionType { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Models/ConnectionModel.cs b/src/modules/Elsa.Connections.Models/ConnectionModel.cs new file mode 100644 index 000000000..6caa8959f --- /dev/null +++ b/src/modules/Elsa.Connections.Models/ConnectionModel.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Elsa.Connections.Models; + +public class ConnectionModel : ConnectionInputModel +{ + /// + /// The Id of the Connection + /// + [Required] public string Id { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Models/ConnectionProperties.cs b/src/modules/Elsa.Connections.Models/ConnectionProperties.cs new file mode 100644 index 000000000..ebed4e6d7 --- /dev/null +++ b/src/modules/Elsa.Connections.Models/ConnectionProperties.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Elsa.Connections.Models; + +public class ConnectionProperties where T : class, new() +{ + /// + /// Creates a new instance of the class. + /// + [JsonConstructor] + public ConnectionProperties() + { + } + + public string? ConnectionName { get; set; } + + [JsonIgnore] + public T Properties { get; set; } = new(); +} + diff --git a/src/modules/Elsa.Connections.Models/Elsa.Connections.Models.csproj b/src/modules/Elsa.Connections.Models/Elsa.Connections.Models.csproj new file mode 100644 index 000000000..1bd63d620 --- /dev/null +++ b/src/modules/Elsa.Connections.Models/Elsa.Connections.Models.csproj @@ -0,0 +1,9 @@ + + + + Provides models that can be shared between the API and API clients + elsa module connections api + Elsa.Connections + + + diff --git a/src/modules/Elsa.Connections.Models/FodyWeavers.xml b/src/modules/Elsa.Connections.Models/FodyWeavers.xml new file mode 100644 index 000000000..ba625ed13 --- /dev/null +++ b/src/modules/Elsa.Connections.Models/FodyWeavers.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/DbContextFactories.cs b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/DbContextFactories.cs new file mode 100644 index 000000000..cd4fca536 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/DbContextFactories.cs @@ -0,0 +1,15 @@ +using Elsa.EntityFrameworkCore.Abstractions; +using Elsa.EntityFrameworkCore.Extensions; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite; + +[UsedImplicitly] +public class SqliteConnectionDbContextFactory : DesignTimeDbContextFactoryBase +{ + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseElsaSqlite(GetType().Assembly, connectionString); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj new file mode 100644 index 000000000..0f350f1e2 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj @@ -0,0 +1,19 @@ + + + + Provides an EF Core migrations for SQLite for the Connection Persistence module. + elsa module persistence efcore entity framework core sqlite + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/FodyWeavers.xml b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/FodyWeavers.xml new file mode 100644 index 000000000..ac6b5b292 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250212220543_V3_3.Designer.cs b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250212220543_V3_3.Designer.cs new file mode 100644 index 000000000..8f3eb3658 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250212220543_V3_3.Designer.cs @@ -0,0 +1,60 @@ +// +using Elsa.Connections.Persistence.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.Migrations +{ + [DbContext(typeof(ConnectionDbContext))] + [Migration("20250212220543_V3_3")] + partial class V3_3 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "8.0.12"); + + modelBuilder.Entity("Elsa.Connections.Persistence.Entities.ConnectionDefinition", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConnectionConfiguration") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConnectionType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_ConnectionDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ConnectionDefinition_TenantId"); + + b.ToTable("ConnectionDefinitions", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250212220543_V3_3.cs b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250212220543_V3_3.cs new file mode 100644 index 000000000..dd64e3a5f --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Migrations/20250212220543_V3_3.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.Migrations +{ + /// + public partial class V3_3 : Migration + { + private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema; + + /// + public V3_3(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: _schema.Schema); + + migrationBuilder.CreateTable( + name: "ConnectionDefinitions", + schema: _schema.Schema, + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + ConnectionConfiguration = table.Column(type: "TEXT", nullable: false), + ConnectionType = table.Column(type: "TEXT", nullable: false), + TenantId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ConnectionDefinitions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ConnectionDefinition_Name", + schema: _schema.Schema, + table: "ConnectionDefinitions", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_ConnectionDefinition_TenantId", + schema: _schema.Schema, + table: "ConnectionDefinitions", + column: "TenantId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ConnectionDefinitions", + schema: _schema.Schema); + } + } +} diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Migrations/ConnectionDbContextModelSnapshot.cs b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Migrations/ConnectionDbContextModelSnapshot.cs new file mode 100644 index 000000000..50c535a68 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/Migrations/ConnectionDbContextModelSnapshot.cs @@ -0,0 +1,57 @@ +// +using Elsa.Connections.Persistence.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.Migrations +{ + [DbContext(typeof(ConnectionDbContext))] + partial class ConnectionDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "8.0.12"); + + modelBuilder.Entity("Elsa.Connections.Persistence.Entities.ConnectionDefinition", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConnectionConfiguration") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ConnectionType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_ConnectionDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ConnectionDefinition_TenantId"); + + b.ToTable("ConnectionDefinitions", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/SqliteProvidersExtensions.cs b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/SqliteProvidersExtensions.cs new file mode 100644 index 000000000..9a38e3722 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/SqliteProvidersExtensions.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using Elsa.Connections.Persistence.EntityFrameworkCore; + +// ReSharper disable once CheckNamespace +namespace Elsa.EntityFrameworkCore.Extensions; + +/// +/// Provides extensions to configure EF Core to use Sqlite. +/// +public static class ConnectionsSqliteProvidersExtensions +{ + private static Assembly Assembly => typeof(ConnectionsSqliteProvidersExtensions).Assembly; + + /// + /// Configures the feature to use Sqlite. + /// + public static EFCoreConnectionPersistenceFeature UseSqlite(this EFCoreConnectionPersistenceFeature feature, string? connectionString = null, ElsaDbContextOptions? options = null) + { + feature.UseSqlite(Assembly, connectionString, options); + return feature; + } + + public static EFCoreConnectionPersistenceFeature UseSqlite(this EFCoreConnectionPersistenceFeature feature, Func connectionStringFunc, ElsaDbContextOptions? options = null) + { + feature.UseSqlite(Assembly, connectionStringFunc, options); + return feature; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/efcore-3.3.sh b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/efcore-3.3.sh new file mode 100644 index 000000000..e5eb4289e --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite/efcore-3.3.sh @@ -0,0 +1 @@ +ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_3 -c ConnectionDbContext -o Migrations" \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Configurations.cs b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Configurations.cs new file mode 100644 index 000000000..cc2a3167d --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Configurations.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Elsa.Connections.Models; +using Elsa.Connections.Persistence.Entities; +using Elsa.EntityFrameworkCore.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Elsa.Connections.Persistence.EntityFrameworkCore; +public class Configurations : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(x => x.ConnectionConfiguration).HasJsonValueConversion(); + builder.HasIndex(x => x.Name).HasDatabaseName($"IX_{nameof(ConnectionDefinition)}_{nameof(ConnectionDefinition.Name)}"); + builder.HasIndex(x => x.TenantId).HasDatabaseName($"IX_{nameof(ConnectionDefinition)}_{nameof(ConnectionDefinition.TenantId)}"); + + } +} diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/DbContext.cs b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/DbContext.cs new file mode 100644 index 000000000..f1b521744 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/DbContext.cs @@ -0,0 +1,34 @@ +using Elsa.Connections.Models; +using Elsa.EntityFrameworkCore; +using Elsa.Connections.Persistence.Entities; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Connections.Persistence.EntityFrameworkCore; + + +/// +/// Db Context for the Connection Module +/// +[UsedImplicitly] +public class ConnectionDbContext : ElsaDbContextBase +{ + /// + public ConnectionDbContext(DbContextOptions options, IServiceProvider serviceProvider) + : base(options, serviceProvider) + { + } + + /// + /// The Connection DB set. + /// + public DbSet ConnectionDefinitions { get; set; } = default!; + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var configuration = new Configurations(); + modelBuilder.ApplyConfiguration(configuration); + base.OnModelCreating(modelBuilder); + } +} diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/EFCoreConnectionStore.cs b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/EFCoreConnectionStore.cs new file mode 100644 index 000000000..bfe4a6203 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/EFCoreConnectionStore.cs @@ -0,0 +1,54 @@ +using Elsa.Connections.Persistence.Contracts; +using Elsa.Connections.Persistence.Entities; +using Elsa.Connections.Persistence.Filters; +using Elsa.EntityFrameworkCore; +using JetBrains.Annotations; + +namespace Elsa.Connections.Persistence.EntityFrameworkCore; + +/// +/// An EF Core implementation of . +/// +[UsedImplicitly] +public class EFCoreConnectionStore(EntityStore store) : IConnectionStore +{ + public Task AddAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default) + { + return store.AddAsync(entity, cancellationToken); + } + + public Task UpdateAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default) + { + return store.UpdateAsync(entity, cancellationToken); + } + + public Task GetAsync(string id, CancellationToken cancellationToken = default) + { + var filter = new ConnectionDefinitionFilter + { + Id = id + }; + + return FindAsync(filter, cancellationToken); + } + + public Task FindAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default) + { + return store.FindAsync(filter.Apply, cancellationToken); + } + + public Task> ListAsync(CancellationToken cancellationToken = default) + { + return store.ListAsync(cancellationToken); + } + + public Task DeleteAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default) + { + return store.DeleteAsync(entity, cancellationToken); + } + + public Task> FindManyAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default) + { + return store.QueryAsync(filter.Apply, cancellationToken); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Elsa.Connections.Persistence.EntityFrameworkCore.csproj b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Elsa.Connections.Persistence.EntityFrameworkCore.csproj new file mode 100644 index 000000000..0931ad805 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Elsa.Connections.Persistence.EntityFrameworkCore.csproj @@ -0,0 +1,13 @@ + + + + Provides an Entity Framework Core provider for the persistence layer of the Connection module + elsa connection persistence efcore entity framework core + + + + + + + + diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Extensions.cs b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Extensions.cs new file mode 100644 index 000000000..718e6446c --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Extensions.cs @@ -0,0 +1,22 @@ +using Elsa.Connections.Persistence.Features; +using Elsa.Connections.Persistence.EntityFrameworkCore; +using JetBrains.Annotations; + +// ReSharper disable once CheckNamespace +namespace Elsa.Agents; + +/// +/// Provides extensions to the feature. +/// +[PublicAPI] +public static class Extensions +{ + /// + /// Configures the to use EF Core persistence providers. + /// + public static ConnectionPersistenceFeature UseEntityFrameworkCore(this ConnectionPersistenceFeature feature, Action? configure = default) + { + feature.Module.Configure(configure); + return feature; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Feature.cs b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Feature.cs new file mode 100644 index 000000000..5d8d9524b --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/Feature.cs @@ -0,0 +1,34 @@ +using Elsa.Connections.Persistence.Entities; +using Elsa.Connections.Persistence.Features; +using Elsa.EntityFrameworkCore; +using Elsa.EntityFrameworkCore.EntityHandlers; +using Elsa.Features.Attributes; +using Elsa.Features.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Connections.Persistence.EntityFrameworkCore; + +/// +/// Configures the default workflow runtime to use EF Core persistence providers. +/// +[DependsOn(typeof(ConnectionPersistenceFeature))] +public class EFCoreConnectionPersistenceFeature(IModule module) : PersistenceFeatureBase(module) +{ + /// + public override void Configure() + { + Module.Configure(feature => + { + feature + .UseConnectionStore(sp=> sp.GetRequiredService()) + ; + }); + } + + /// + public override void Apply() + { + base.Apply(); + AddEntityStore(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/FodyWeavers.xml b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/FodyWeavers.xml new file mode 100644 index 000000000..ac6b5b292 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence.EntityFrameworkCore/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence/Contracts/IConnectionStore.cs b/src/modules/Elsa.Connections.Persistence/Contracts/IConnectionStore.cs new file mode 100644 index 000000000..1e4b63cb4 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence/Contracts/IConnectionStore.cs @@ -0,0 +1,42 @@ +using Elsa.Connections.Persistence.Filters; +using Elsa.Connections.Persistence.Entities; + +namespace Elsa.Connections.Persistence.Contracts; + +public interface IConnectionStore +{ + /// + /// Get The Connection from the Store + /// + public Task GetAsync(string id, CancellationToken cancellationToken = default); + + /// + /// Get All Connections from the store + /// + public Task> ListAsync(CancellationToken cancellationToken = default); + + /// + /// Get All connection using Filter + /// + public Task FindAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default); + + /// + /// Get All connection using Filter + /// + public Task> FindManyAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default); + + /// + /// Add a new entity to the store + /// + public Task AddAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default); + + /// + /// Update the entity to the store + /// + public Task UpdateAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default); + + /// + /// Delete the entity from the store + /// + public Task DeleteAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default); +} diff --git a/src/modules/Elsa.Connections.Persistence/Elsa.Connections.Persistence.csproj b/src/modules/Elsa.Connections.Persistence/Elsa.Connections.Persistence.csproj new file mode 100644 index 000000000..cf7ec74b8 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence/Elsa.Connections.Persistence.csproj @@ -0,0 +1,13 @@ + + + + Provides Connections persistence services + elsa module connections persistence + + + + + + + + diff --git a/src/modules/Elsa.Connections.Persistence/Entities/ConnectionDefinition.cs b/src/modules/Elsa.Connections.Persistence/Entities/ConnectionDefinition.cs new file mode 100644 index 000000000..15a1e46ff --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence/Entities/ConnectionDefinition.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Nodes; +using Elsa.Common.Entities; + +namespace Elsa.Connections.Persistence.Entities; + +public class ConnectionDefinition : Entity +{ + /// + /// The Name of the Connection + /// + public string Name { get; set; } = null!; + + /// + /// The Description of the Connection + /// + public string? Description { get; set; } + + /// + /// The Configuration of the Connection in a Json Format + /// + public JsonObject ConnectionConfiguration { get; set; } = new(); + + /// + /// The Connection Type Name + /// + public string ConnectionType { get; set; } = null!; +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence/Extensions/ConnectionPersistenceExtensions.cs b/src/modules/Elsa.Connections.Persistence/Extensions/ConnectionPersistenceExtensions.cs new file mode 100644 index 000000000..af3eb822b --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence/Extensions/ConnectionPersistenceExtensions.cs @@ -0,0 +1,19 @@ +using Elsa.Connections.Persistence.Features; +using Elsa.Features.Services; + +// ReSharper disable once CheckNamespace +namespace Elsa.Extensions; + +/// +/// Extends with methods to use Connection Persistence Feature. +/// +public static class ConnectionPersistenceExtensions +{ + /// + /// Installs the persistence feature for the Connection module. + /// + public static IModule UseConnectionPersistence(this IModule module, Action? configure = null) + { + return module.Use(configure); + } +} diff --git a/src/modules/Elsa.Connections.Persistence/Features/ConnectionPersistenceFeature.cs b/src/modules/Elsa.Connections.Persistence/Features/ConnectionPersistenceFeature.cs new file mode 100644 index 000000000..b93658e06 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence/Features/ConnectionPersistenceFeature.cs @@ -0,0 +1,28 @@ +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using Microsoft.Extensions.DependencyInjection; +using Elsa.Connections.Persistence.Contracts; +using Elsa.Connections.Persistence.Services; +using Elsa.Connections.Persistence.Entities; + +namespace Elsa.Connections.Persistence.Features; + +public class ConnectionPersistenceFeature(IModule module) : FeatureBase(module) +{ + private Func _connectionStoreFactory = sp => sp.GetRequiredService(); + + public ConnectionPersistenceFeature UseConnectionStore(Func factory) + { + _connectionStoreFactory = factory; + return this; + } + + public override void Apply() + { + Services.AddScoped(_connectionStoreFactory); + + Services + .AddMemoryStore(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence/Filters/ConnectionDefinitionFilter.cs b/src/modules/Elsa.Connections.Persistence/Filters/ConnectionDefinitionFilter.cs new file mode 100644 index 000000000..021a519cb --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence/Filters/ConnectionDefinitionFilter.cs @@ -0,0 +1,19 @@ +using Elsa.Connections.Persistence.Entities; + +namespace Elsa.Connections.Persistence.Filters; +public class ConnectionDefinitionFilter +{ + public string? Id { get; set; } + public string? Name { get; set; } + public string? NotId { get; set; } + public string? Type { get; set; } + + public IQueryable Apply(IQueryable queryable) + { + if(!string.IsNullOrEmpty(Id)) queryable = queryable.Where(x=>x.Id == Id); + if (!string.IsNullOrWhiteSpace(NotId)) queryable = queryable.Where(x => x.Id != NotId); + if (!string.IsNullOrWhiteSpace(Name)) queryable = queryable.Where(x=> x.Name == Name); + if(!string.IsNullOrWhiteSpace(Type)) queryable = queryable.Where(x=>x.ConnectionType == Type); + return queryable; + } +} diff --git a/src/modules/Elsa.Connections.Persistence/FodyWeavers.xml b/src/modules/Elsa.Connections.Persistence/FodyWeavers.xml new file mode 100644 index 000000000..ac6b5b292 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Connections.Persistence/Services/InMemoryConnectionStore.cs b/src/modules/Elsa.Connections.Persistence/Services/InMemoryConnectionStore.cs new file mode 100644 index 000000000..3a9af9fa7 --- /dev/null +++ b/src/modules/Elsa.Connections.Persistence/Services/InMemoryConnectionStore.cs @@ -0,0 +1,52 @@ +using Elsa.Common.Services; +using Elsa.Connections.Persistence.Contracts; +using Elsa.Connections.Persistence.Filters; +using Elsa.Connections.Persistence.Entities; +using JetBrains.Annotations; + +namespace Elsa.Connections.Persistence.Services; + +[UsedImplicitly] +public class InMemoryConnectionStore(MemoryStore memoryStore) : IConnectionStore +{ + public Task AddAsync(ConnectionDefinition model, CancellationToken cancellationToken = default) + { + memoryStore.Add(model, x => x.Id); + return Task.CompletedTask; + } + + public Task DeleteAsync(ConnectionDefinition model, CancellationToken cancellationToken = default) + { + memoryStore.Delete(model.Id); + return Task.CompletedTask; + } + + public Task GetAsync(string id, CancellationToken cancellationToken = default) + { + var result = memoryStore.Find(c => c.Id == id); + return Task.FromResult(result); + } + + public Task> ListAsync(CancellationToken cancellationToken) + { + var results = memoryStore.List(); + return Task.FromResult(results); + } + public Task FindAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default) + { + var entity = memoryStore.Query(filter.Apply).FirstOrDefault(); + return Task.FromResult(entity); + } + + public Task> FindManyAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default) + { + var entities = memoryStore.Query(filter.Apply); + return Task.FromResult(entities); + } + + public Task UpdateAsync(ConnectionDefinition model, CancellationToken cancellationToken = default) + { + memoryStore.Update(model, x => x.Id); + return Task.CompletedTask; + } +} diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs index c64611074..14e407125 100644 --- a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithVariablesAndInputOutputAccessors.cs @@ -1,7 +1,6 @@ using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.JavaScript.Extensions; -using Elsa.JavaScript.Helpers; using Elsa.JavaScript.Notifications; using Elsa.JavaScript.Options; using Elsa.Mediator.Contracts; diff --git a/src/modules/Elsa.Workflows.Core/Extensions/DictionaryExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/DictionaryExtensions.cs index bc1bb6c44..33cb94f5f 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/DictionaryExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/DictionaryExtensions.cs @@ -57,7 +57,7 @@ public static class DictionaryExtensions public static T? GetValueOrDefault(this IDictionary dictionary, string key, Func defaultValueFactory) => TryGetValue(dictionary, key, out var value) ? value : defaultValueFactory(); public static T? GetValueOrDefault(this IDictionary dictionary, IEnumerable keys, Func defaultValueFactory) => TryGetValue(dictionary, keys, out var value) ? value : defaultValueFactory(); public static T? GetValueOrDefault(this IDictionary dictionary, string key) => GetValueOrDefault(dictionary, key, () => default); - public static object? GetValueOrDefault(this IDictionary dictionary, string key) => GetValueOrDefault(dictionary, key, () => default); + public static object? GetValueOrDefault(this IDictionary dictionary, string key) => GetValueOrDefault(dictionary, key, () => null); public static T GetOrAdd(this IDictionary dictionary, TKey key, Func valueFactory) { diff --git a/src/modules/Elsa.Workflows.Core/UIHints/Dropdown/DropDownOptionsProviderBase.cs b/src/modules/Elsa.Workflows.Core/UIHints/Dropdown/DropDownOptionsProviderBase.cs index b97dc3385..1fccfe0dc 100644 --- a/src/modules/Elsa.Workflows.Core/UIHints/Dropdown/DropDownOptionsProviderBase.cs +++ b/src/modules/Elsa.Workflows.Core/UIHints/Dropdown/DropDownOptionsProviderBase.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Elsa.Extensions; namespace Elsa.Workflows.UIHints.Dropdown; @@ -19,7 +20,9 @@ public abstract class DropDownOptionsProviderBase : IPropertyUIHandler var options = new Dictionary { [InputUIHints.DropDown] = props - }; + }; + + options.AddRange(GetUIPropertyAdditionalOptions()); return options; } @@ -27,5 +30,14 @@ public abstract class DropDownOptionsProviderBase : IPropertyUIHandler /// /// Implement this to provide items to the dropdown list. /// - protected abstract ValueTask> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken); + protected abstract ValueTask> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken); + + /// + /// Override this to provide additional options to the list + /// + /// + protected virtual IDictionary GetUIPropertyAdditionalOptions() + { + return new Dictionary(); + } } \ No newline at end of file