Merge pull request #6291 from jdevillard/enh/Connections

FEAT : Connection/Secret provider/handler to define generic configuration for Activities
This commit is contained in:
Sipke Schoorstra 2025-02-23 14:07:55 +01:00 committed by GitHub
commit 6f5b7e77d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 1622 additions and 4 deletions

View file

@ -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}

View file

@ -13,6 +13,10 @@
<ProjectReference Include="..\..\modules\Elsa.Agents.Persistence.EntityFrameworkCore.PostgreSql\Elsa.Agents.Persistence.EntityFrameworkCore.PostgreSql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Agents.Persistence.EntityFrameworkCore.Sqlite\Elsa.Agents.Persistence.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Agents.Persistence.EntityFrameworkCore.SqlServer\Elsa.Agents.Persistence.EntityFrameworkCore.SqlServer.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Connections.Api\Elsa.Connections.Api.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Connections.Core\Elsa.Connections.Core.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite\Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Connections.Persistence.EntityFrameworkCore\Elsa.Connections.Persistence.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.PostgreSql\Elsa.EntityFrameworkCore.PostgreSql.csproj" />
<ProjectReference Include="..\..\modules\Elsa.EntityFrameworkCore.SqlServer\Elsa.EntityFrameworkCore.SqlServer.csproj" />
<ProjectReference Include="..\..\modules\Elsa.JavaScript.Libraries\Elsa.JavaScript.Libraries.csproj" />

View file

@ -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<Program>());
elsa.UseConnectionPersistence(ef=> ef.UseEntityFrameworkCore(f=>f.UseSqlite()));
elsa.UseConnectionsApi();
});
services.Configure<WebhookSinksOptions>(options => configuration.GetSection("Webhooks").Bind(options));

View file

@ -0,0 +1,5 @@
namespace Elsa.Connections.Api;
public class Constants
{
public const string PermissionsNamespace = "connections";
}

View file

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides Activity Connection API endpoints</Description>
<PackageTags>elsa module activity connection abstraction api</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj" />
<ProjectReference Include="..\Elsa.Connections.Core\Elsa.Connections.Core.csproj" />
<ProjectReference Include="..\Elsa.Connections.Models\Elsa.Connections.Models.csproj" />
<ProjectReference Include="..\Elsa.Connections.Persistence\Elsa.Connections.Persistence.csproj" />
</ItemGroup>
</Project>

View file

@ -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<IEnumerable<InputDescriptor>>
{
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<string>("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);
}
}

View file

@ -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<PagedListResponse<ConnectionDescriptor>>
{
public override void Configure()
{
Get("/connection-configuration/descriptors");
ConfigurePermissions($"{Constants.PermissionsNamespace}/descriptor:read");
}
public override Task<PagedListResponse<ConnectionDescriptor>> ExecuteAsync(CancellationToken ct)
{
var descriptors = registry.ListAll().ToList();
return Task.FromResult(new PagedListResponse<ConnectionDescriptor>(Page.Of(descriptors, descriptors.Count)) );
}
}

View file

@ -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<ConnectionInputModel, ConnectionModel>
{
public override void Configure()
{
Post("/connection-configuration");
ConfigurePermissions($"{Constants.PermissionsNamespace}:write");
}
public override async Task<ConnectionModel> 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<bool> IsNameUniqueAsync(string name, CancellationToken ct)
{
var filter = new ConnectionDefinitionFilter
{
Name = name
};
return await store.FindAsync(filter, ct) == null;
}
}

View file

@ -0,0 +1,27 @@
using Elsa.Abstractions;
using Elsa.Connections.Persistence.Contracts;
namespace Elsa.Connections.Api.Endpoints.Delete;
public class Endpoint(IConnectionStore store) : ElsaEndpoint<Request>
{
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);
}
}

View file

@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace Elsa.Connections.Api.Endpoints.Delete;
public class Request
{
[Required] public string Id { get; set; } = null!;
}

View file

@ -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<Request,ConnectionModel>
{
public override void Configure()
{
Get("/connection-configuration/{id}");
ConfigurePermissions($"{Constants.PermissionsNamespace}:read");
}
public override async Task<ConnectionModel> 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();
}
}

View file

@ -0,0 +1,8 @@
using System.ComponentModel.DataAnnotations;
namespace Elsa.Connections.Api.Endpoints.Get;
public class Request
{
[Required] public string Id { get; set; } = null!;
}

View file

@ -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<PagedListResponse<ConnectionModel>>
{
public override void Configure()
{
Get("/connection-configuration");
ConfigurePermissions($"{Constants.PermissionsNamespace}:read");
}
public override async Task<PagedListResponse<ConnectionModel>> ExecuteAsync(CancellationToken ct)
{
var entities = await store.ListAsync(ct);
var models = entities.Select(x => x.ToModel()).ToList();
return new(Page.Of(models, models.Count()));
}
}

View file

@ -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<ConnectionInputModel, ConnectionModel>
{
public override void Configure()
{
Put("/connection-configuration/{id}");
ConfigurePermissions($"{Constants.PermissionsNamespace}:write");
}
public override async Task<ConnectionModel> ExecuteAsync(ConnectionInputModel model, CancellationToken ct)
{
var id = Route<string>("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<bool> IsNameDuplicateAsync(string name, string id, CancellationToken cancellationToken)
{
var entities = await store.FindAsync(new()
{
NotId = id,
Name = name
}, cancellationToken);
return entities != null;
}
}

View file

@ -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,
};
}
}

View file

@ -0,0 +1,19 @@
using Elsa.Connections.Api.Features;
using Elsa.Features.Services;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Extends <see cref="IModule"/> with methods to Connection API endpoints.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Installs the Connections API feature.
/// </summary>
public static IModule UseConnectionsApi(this IModule module, Action<ConnectionsApiFeature>? configure = null)
{
return module.Use(configure);
}
}

View file

@ -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;
/// <summary>
/// A feature that installs API endpoints to interact connections.
/// </summary>
[DependsOn(typeof(ConnectionPersistenceFeature))]
[UsedImplicitly]
public class ConnectionsApiFeature(IModule module) : FeatureBase(module)
{
/// <inheritdoc />
public override void Configure()
{
Module.AddFastEndpointsAssembly<ConnectionsApiFeature>();
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,10 @@
namespace Elsa.Connections.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public class ConnectionActivityAttribute(string type) : Attribute
{
/// <summary>
/// The TypeName of the connection
/// </summary>
public string Type { get; set; } = type;
}

View file

@ -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;
}

View file

@ -0,0 +1,13 @@
namespace Elsa.Connections.Attributes;
public class ConnectionTypeAttribute : Attribute
{
public ConnectionTypeAttribute(Type type)
{
Type = type;
}
/// <summary>
/// The TypeName of the connection
/// </summary>
public Type Type { get; set; }
}

View file

@ -0,0 +1,5 @@
namespace Elsa.Connections.Attributes;
public class NoLogAttribute : Attribute
{
}

View file

@ -0,0 +1,40 @@
using Elsa.Connections.Models;
using Elsa.Workflows.Models;
namespace Elsa.Connections.Contracts;
/// <summary>
/// Store all connection descriptors available to the system
/// </summary>
public interface IConnectionDescriptorRegistry
{
/// <summary>
/// Adds a connection descriptor to the registry
/// </summary>
/// <param name="connectionType">The type of the connection </param>
/// <param name="connectionDescriptor">The type desiptor of the connection</param>
void Add(Type connectionType, ConnectionDescriptor connectionDescriptor);
/// <summary>
/// Removes an activity descriptor from the registry.
/// </summary>
/// <param name="connectionType">The type of the connection.</param>
/// <param name="descriptor"></param>
void Remove(Type connectionType, ActivityDescriptor descriptor);
/// <summary>
/// Returns all connection descriptors in the registry.
/// </summary>
/// <returns>All connection descriptors in the registry.</returns>
IEnumerable<ConnectionDescriptor> ListAll();
Type? Get(string type);
/// <summary>
/// Get the Input Descriptors for an connection
/// </summary>
/// <param name="activityType">The type of the connectn</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<IEnumerable<InputDescriptor>> GetConnectionDescriptorAsync(string activityType, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,6 @@
namespace Elsa.Connections.Contracts;
public interface IConnectionDescriptorRegistryEX
{
}

View file

@ -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; }
}

View file

@ -0,0 +1,29 @@
using System.Text.Json;
namespace Elsa.Connections.Converters;
public class Int32Converter : System.Text.Json.Serialization.JsonConverter<int>
{
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);
}
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides an connection framework to use connection abtraction in activities</Description>
<PackageTags>elsa module connection abstratction</PackageTags>
<RootNamespace>Elsa.Connections</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Common\Elsa.Common.csproj" />
<ProjectReference Include="..\Elsa.Connections.Models\Elsa.Connections.Models.csproj" />
<ProjectReference Include="..\Elsa.Connections.Persistence\Elsa.Connections.Persistence.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,24 @@
// ReSharper disable once CheckNamespace
using Microsoft.Extensions.Logging;
namespace Elsa.Connections.Core.Extensions;
/// <summary>
///
/// </summary>
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);
}

View file

@ -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;
/// <summary>
/// Extends <see cref="IModule"/> with methods to install Semantic Kernel API endpoints.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Installs the Semantic Kernel API feature.
/// </summary>
public static IModule UseConnections(this IModule module, Action<ConnectionsFeatures>? configure = null)
{
return module.Use(configure);
}
}
/// <summary>
/// Adds extension methods to <see cref="ConnectionMiddleware"/>.
/// </summary>
public static class ConnectionMiddlewareExtensions
{
/// <summary>
/// Installs the <see cref="ConnectionMiddleware"/> component in the activity execution pipeline.
/// </summary>
public static IActivityExecutionPipelineBuilder UseConnectionMiddleware(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<ConnectionMiddleware>();
}

View file

@ -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;
/// <summary>
/// A feature that installs API endpoints to interact with skilled agents.
/// </summary>
[DependsOn(typeof(WorkflowsFeature))]
[UsedImplicitly]
public class ConnectionsFeatures(IModule module) : FeatureBase(module)
{
/// <summary>
/// A set of connection types to make available to the system.
/// </summary>
public HashSet<Type> ConnectionTypes { get; } = [];
/// <inheritdoc />
public override void Apply()
{
// Obfuscate Connection Properties.
Services.AddActivityStateFilter<PropertyAttributeFilter>();
// Activity property options providers.
Services.AddScoped<Elsa.Workflows.IPropertyUIHandler, ConnectionOptionsProvider>();
Services.AddSingleton<IConnectionDescriptorRegistry, ConnectionRegistry>();
//UIHints
Services.AddScoped<IUIHintHandler, ConnectionDropDownUIHintHandler>();
Services.Configure<ConnectionOptions>(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<WorkflowsFeature>()
.WithDefaultActivityExecutionPipeline(pipeline => pipeline.Insert<ConnectionMiddleware>(3));
base.Configure();
}
public ConnectionsFeatures AddConnectionsFrom<TMarker>()
{
var connectionTypes = typeof(TMarker).Assembly.GetExportedTypes()
.Where(x => x.GetCustomAttribute<ConnectionPropertyAttribute>() != null )
.ToList();
ConnectionTypes.AddRange(connectionTypes);
return this;
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -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;
/// <summary>
/// An activity execution middleware component that injects connection properties from Connection Name.
/// </summary>
[UsedImplicitly]
public class ConnectionMiddleware(ActivityMiddlewareDelegate next
, IConnectionStore connectionStore
,ILogger<ConnectionMiddleware> logger)
: IActivityExecutionMiddleware
{
private static JsonSerializerOptions? _serializerOptions;
private static JsonSerializerOptions SerializerOptions =>
_serializerOptions ??= new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}.WithConverters(new Int32Converter());
/// <inheritdoc />
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);
}
}

View file

@ -0,0 +1,9 @@
namespace Elsa.Connections.Models;
public class ConnectionOptions
{
/// <summary>
/// A collection of connection types that are available to the system.
/// </summary>
public HashSet<Type> ConnectionTypes { get; set; } = new();
}

View file

@ -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<IDictionary<string, object>> 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<ICollection<SelectListItem>> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken)
{
var connection = new List<SelectListItem>();
var connectionType = propertyInfo.GetCustomAttribute<ConnectionTypeAttribute>()?.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<string, object> GetUIPropertyAdditionalOptions()
{
return new Dictionary<string, object>
{
["Refresh"] = true
};
}
}

View file

@ -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<ConnectionOptions> options, IActivityDescriber describer) : IConnectionDescriptorRegistry
{
private readonly ConcurrentDictionary<Type, ConnectionDescriptor> _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<ConnectionDescriptor> 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<IEnumerable<InputDescriptor>> 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<IEnumerable<InputDescriptor>> DescribeInputPropertiesAsync(IEnumerable<PropertyInfo> 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<ConnectionPropertyAttribute>();
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
);
}
}

View file

@ -0,0 +1,21 @@
using System.Reflection;
using Elsa.Workflows;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.UIHints.Dropdown;
namespace Elsa.Connections.UIHints;
/// <summary>
/// UIHint Handler that use DropDown for Connection Property
/// </summary>
public class ConnectionDropDownUIHintHandler : IUIHintHandler
{
/// <inheritdoc />
public string UIHint => $"connection-{InputUIHints.DropDown}";
/// <inheritdoc />
public ValueTask<IEnumerable<Type>> GetPropertyUIHandlersAsync(PropertyInfo propertyInfo, CancellationToken cancellationToken)
{
return new([typeof(StaticDropDownOptionsProvider)]);
}
}

View file

@ -0,0 +1,3 @@
namespace Elsa.Connections.Models;
public record ConnectionDescriptor(string Type, string? Description, string? Namespace, string? ProviderName);

View file

@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Nodes;
namespace Elsa.Connections.Models;
public class ConnectionInputModel
{
/// <summary>
/// The Name of the Connection
/// </summary>
[Required] public string Name { get; set; } = null!;
/// <summary>
/// The Description of the Connection
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The Configuration of the Connection in a Json Format
/// </summary>
public JsonObject ConnectionConfiguration { get; set; } = new();
/// <summary>
/// The Connection Type Name
/// </summary>
[Required] public string ConnectionType { get; set; } = null!;
}

View file

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace Elsa.Connections.Models;
public class ConnectionModel : ConnectionInputModel
{
/// <summary>
/// The Id of the Connection
/// </summary>
[Required] public string Id { get; set; } = null!;
}

View file

@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace Elsa.Connections.Models;
public class ConnectionProperties<T> where T : class, new()
{
/// <summary>
/// Creates a new instance of the <see cref="ConnectionProperties"/> class.
/// </summary>
[JsonConstructor]
public ConnectionProperties()
{
}
public string? ConnectionName { get; set; }
[JsonIgnore]
public T Properties { get; set; } = new();
}

View file

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides models that can be shared between the API and API clients</Description>
<PackageTags>elsa module connections api</PackageTags>
<RootNamespace>Elsa.Connections</RootNamespace>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,2 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
</Weavers>

View file

@ -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<ConnectionDbContext>
{
protected override void ConfigureBuilder(DbContextOptionsBuilder<ConnectionDbContext> builder, string connectionString)
{
builder.UseElsaSqlite(GetType().Assembly, connectionString);
}
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides an EF Core migrations for SQLite for the Connection Persistence module.</Description>
<PackageTags>elsa module persistence efcore entity framework core sqlite</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Connections.Persistence.EntityFrameworkCore\Elsa.Connections.Persistence.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,60 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConnectionConfiguration")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ConnectionType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("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
}
}
}

View file

@ -0,0 +1,62 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.Migrations
{
/// <inheritdoc />
public partial class V3_3 : Migration
{
private readonly Elsa.EntityFrameworkCore.IElsaDbContextSchema _schema;
/// <inheritdoc />
public V3_3(Elsa.EntityFrameworkCore.IElsaDbContextSchema schema)
{
_schema = schema;
}
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: _schema.Schema);
migrationBuilder.CreateTable(
name: "ConnectionDefinitions",
schema: _schema.Schema,
columns: table => new
{
Id = table.Column<string>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: true),
ConnectionConfiguration = table.Column<string>(type: "TEXT", nullable: false),
ConnectionType = table.Column<string>(type: "TEXT", nullable: false),
TenantId = table.Column<string>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ConnectionDefinitions",
schema: _schema.Schema);
}
}
}

View file

@ -0,0 +1,57 @@
// <auto-generated />
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<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConnectionConfiguration")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ConnectionType")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("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
}
}
}

View file

@ -0,0 +1,28 @@
using System.Reflection;
using Elsa.Connections.Persistence.EntityFrameworkCore;
// ReSharper disable once CheckNamespace
namespace Elsa.EntityFrameworkCore.Extensions;
/// <summary>
/// Provides extensions to configure EF Core to use Sqlite.
/// </summary>
public static class ConnectionsSqliteProvidersExtensions
{
private static Assembly Assembly => typeof(ConnectionsSqliteProvidersExtensions).Assembly;
/// <summary>
/// Configures the feature to use Sqlite.
/// </summary>
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<IServiceProvider, string> connectionStringFunc, ElsaDbContextOptions? options = null)
{
feature.UseSqlite(Assembly, connectionStringFunc, options);
return feature;
}
}

View file

@ -0,0 +1 @@
ef-migration-runtime-schema --interface Elsa.EntityFrameworkCore.IElsaDbContextSchema --efOptions "migrations add V3_3 -c ConnectionDbContext -o Migrations"

View file

@ -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<ConnectionDefinition>
{
public void Configure(EntityTypeBuilder<ConnectionDefinition> 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)}");
}
}

View file

@ -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;
/// <summary>
/// Db Context for the Connection Module
/// </summary>
[UsedImplicitly]
public class ConnectionDbContext : ElsaDbContextBase
{
/// <inheritdoc />
public ConnectionDbContext(DbContextOptions<ConnectionDbContext> options, IServiceProvider serviceProvider)
: base(options, serviceProvider)
{
}
/// <summary>
/// The Connection DB set.
/// </summary>
public DbSet<ConnectionDefinition> ConnectionDefinitions { get; set; } = default!;
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var configuration = new Configurations();
modelBuilder.ApplyConfiguration<ConnectionDefinition>(configuration);
base.OnModelCreating(modelBuilder);
}
}

View file

@ -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;
/// <summary>
/// An EF Core implementation of <see cref="IConnectionStore"/>.
/// </summary>
[UsedImplicitly]
public class EFCoreConnectionStore(EntityStore<ConnectionDbContext, ConnectionDefinition> 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<ConnectionDefinition?> GetAsync(string id, CancellationToken cancellationToken = default)
{
var filter = new ConnectionDefinitionFilter
{
Id = id
};
return FindAsync(filter, cancellationToken);
}
public Task<ConnectionDefinition?> FindAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default)
{
return store.FindAsync(filter.Apply, cancellationToken);
}
public Task<IEnumerable<ConnectionDefinition>> ListAsync(CancellationToken cancellationToken = default)
{
return store.ListAsync(cancellationToken);
}
public Task DeleteAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default)
{
return store.DeleteAsync(entity, cancellationToken);
}
public Task<IEnumerable<ConnectionDefinition>> FindManyAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default)
{
return store.QueryAsync(filter.Apply, cancellationToken);
}
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides an Entity Framework Core provider for the persistence layer of the Connection module</Description>
<PackageTags>elsa connection persistence efcore entity framework core</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Connections.Persistence\Elsa.Connections.Persistence.csproj" />
<ProjectReference Include="..\Elsa.EntityFrameworkCore.Common\Elsa.EntityFrameworkCore.Common.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,22 @@
using Elsa.Connections.Persistence.Features;
using Elsa.Connections.Persistence.EntityFrameworkCore;
using JetBrains.Annotations;
// ReSharper disable once CheckNamespace
namespace Elsa.Agents;
/// <summary>
/// Provides extensions to the <see cref="ConnectionPersistenceFeature"/> feature.
/// </summary>
[PublicAPI]
public static class Extensions
{
/// <summary>
/// Configures the <see cref="ConnectionPersistenceFeature"/> to use EF Core persistence providers.
/// </summary>
public static ConnectionPersistenceFeature UseEntityFrameworkCore(this ConnectionPersistenceFeature feature, Action<EFCoreConnectionPersistenceFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
}

View file

@ -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;
/// <summary>
/// Configures the default workflow runtime to use EF Core persistence providers.
/// </summary>
[DependsOn(typeof(ConnectionPersistenceFeature))]
public class EFCoreConnectionPersistenceFeature(IModule module) : PersistenceFeatureBase<EFCoreConnectionPersistenceFeature, ConnectionDbContext>(module)
{
/// <inheritdoc />
public override void Configure()
{
Module.Configure<ConnectionPersistenceFeature>(feature =>
{
feature
.UseConnectionStore(sp=> sp.GetRequiredService<EFCoreConnectionStore>())
;
});
}
/// <inheritdoc />
public override void Apply()
{
base.Apply();
AddEntityStore<ConnectionDefinition, EFCoreConnectionStore>();
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -0,0 +1,42 @@
using Elsa.Connections.Persistence.Filters;
using Elsa.Connections.Persistence.Entities;
namespace Elsa.Connections.Persistence.Contracts;
public interface IConnectionStore
{
/// <summary>
/// Get The Connection from the Store
/// </summary>
public Task<ConnectionDefinition?> GetAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Get All Connections from the store
/// </summary>
public Task<IEnumerable<ConnectionDefinition>> ListAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Get All connection using Filter
/// </summary>
public Task<ConnectionDefinition?> FindAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default);
/// <summary>
/// Get All connection using Filter
/// </summary>
public Task<IEnumerable<ConnectionDefinition>> FindManyAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default);
/// <summary>
/// Add a new entity to the store
/// </summary>
public Task AddAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default);
/// <summary>
/// Update the entity to the store
/// </summary>
public Task UpdateAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default);
/// <summary>
/// Delete the entity from the store
/// </summary>
public Task DeleteAsync(ConnectionDefinition entity, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>Provides Connections persistence services</Description>
<PackageTags>elsa module connections persistence </PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Common\Elsa.Common.csproj" />
<ProjectReference Include="..\Elsa.Connections.Models\Elsa.Connections.Models.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,27 @@
using System.Text.Json.Nodes;
using Elsa.Common.Entities;
namespace Elsa.Connections.Persistence.Entities;
public class ConnectionDefinition : Entity
{
/// <summary>
/// The Name of the Connection
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// The Description of the Connection
/// </summary>
public string? Description { get; set; }
/// <summary>
/// The Configuration of the Connection in a Json Format
/// </summary>
public JsonObject ConnectionConfiguration { get; set; } = new();
/// <summary>
/// The Connection Type Name
/// </summary>
public string ConnectionType { get; set; } = null!;
}

View file

@ -0,0 +1,19 @@
using Elsa.Connections.Persistence.Features;
using Elsa.Features.Services;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Extends <see cref="IModule"/> with methods to use Connection Persistence Feature.
/// </summary>
public static class ConnectionPersistenceExtensions
{
/// <summary>
/// Installs the persistence feature for the Connection module.
/// </summary>
public static IModule UseConnectionPersistence(this IModule module, Action<ConnectionPersistenceFeature>? configure = null)
{
return module.Use(configure);
}
}

View file

@ -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<IServiceProvider, IConnectionStore> _connectionStoreFactory = sp => sp.GetRequiredService<InMemoryConnectionStore>();
public ConnectionPersistenceFeature UseConnectionStore(Func<IServiceProvider, IConnectionStore> factory)
{
_connectionStoreFactory = factory;
return this;
}
public override void Apply()
{
Services.AddScoped(_connectionStoreFactory);
Services
.AddMemoryStore<ConnectionDefinition, InMemoryConnectionStore>();
}
}

View file

@ -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<ConnectionDefinition> Apply(IQueryable<ConnectionDefinition> 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;
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>

View file

@ -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<ConnectionDefinition> 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<ConnectionDefinition?> GetAsync(string id, CancellationToken cancellationToken = default)
{
var result = memoryStore.Find(c => c.Id == id);
return Task.FromResult(result);
}
public Task<IEnumerable<ConnectionDefinition>> ListAsync(CancellationToken cancellationToken)
{
var results = memoryStore.List();
return Task.FromResult(results);
}
public Task<ConnectionDefinition?> FindAsync(ConnectionDefinitionFilter filter, CancellationToken cancellationToken = default)
{
var entity = memoryStore.Query(filter.Apply).FirstOrDefault();
return Task.FromResult(entity);
}
public Task<IEnumerable<ConnectionDefinition>> 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;
}
}

View file

@ -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;

View file

@ -57,7 +57,7 @@ public static class DictionaryExtensions
public static T? GetValueOrDefault<T>(this IDictionary<string, object> dictionary, string key, Func<T?> defaultValueFactory) => TryGetValue<T>(dictionary, key, out var value) ? value : defaultValueFactory();
public static T? GetValueOrDefault<T>(this IDictionary<string, object> dictionary, IEnumerable<string> keys, Func<T?> defaultValueFactory) => TryGetValue<T>(dictionary, keys, out var value) ? value : defaultValueFactory();
public static T? GetValueOrDefault<T>(this IDictionary<string, object> dictionary, string key) => GetValueOrDefault<T>(dictionary, key, () => default);
public static object? GetValueOrDefault(this IDictionary<string, object> dictionary, string key) => GetValueOrDefault<object>(dictionary, key, () => default);
public static object? GetValueOrDefault(this IDictionary<string, object> dictionary, string key) => GetValueOrDefault<object>(dictionary, key, () => null);
public static T GetOrAdd<TKey, T>(this IDictionary<TKey, T> dictionary, TKey key, Func<T> valueFactory)
{

View file

@ -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<string, object>
{
[InputUIHints.DropDown] = props
};
};
options.AddRange(GetUIPropertyAdditionalOptions());
return options;
}
@ -27,5 +30,14 @@ public abstract class DropDownOptionsProviderBase : IPropertyUIHandler
/// <summary>
/// Implement this to provide items to the dropdown list.
/// </summary>
protected abstract ValueTask<ICollection<SelectListItem>> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken);
protected abstract ValueTask<ICollection<SelectListItem>> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken);
/// <summary>
/// Override this to provide additional options to the list
/// </summary>
/// <returns></returns>
protected virtual IDictionary<string, object> GetUIPropertyAdditionalOptions()
{
return new Dictionary<String, object>();
}
}