From c4e04c4434c297f9e00bb991c3c7cb22b75a4f99 Mon Sep 17 00:00:00 2001 From: Matthew Knibbs Date: Wed, 1 Jan 2025 23:14:09 +0000 Subject: [PATCH] Adds SQL Activities. Includes implimentations for MS SQL Server, PostgreSql, MySql and Sqlite. --- Directory.Packages.props | 302 +++++++++--------- Elsa.sln | 38 +++ .../Elsa.Server.Web/Elsa.Server.Web.csproj | 79 ++--- src/apps/Elsa.Server.Web/Program.cs | 15 + .../Elsa.Sql.MySql/Elsa.Sql.MySql.csproj | 18 ++ src/modules/Elsa.Sql.MySql/FodyWeavers.xml | 3 + src/modules/Elsa.Sql.MySql/MySqlClient.cs | 55 ++++ .../Elsa.Sql.PostgreSql.csproj | 18 ++ .../Elsa.Sql.PostgreSql/FodyWeavers.xml | 3 + .../Elsa.Sql.PostgreSql/PostgreSqlClient.cs | 55 ++++ .../Elsa.Sql.SqlServer.csproj | 18 ++ .../Elsa.Sql.SqlServer/FodyWeavers.xml | 3 + .../Elsa.Sql.SqlServer/SqlServerClient.cs | 55 ++++ .../Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj | 18 ++ src/modules/Elsa.Sql.Sqlite/FodyWeavers.xml | 3 + src/modules/Elsa.Sql.Sqlite/SqliteClient.cs | 55 ++++ src/modules/Elsa.Sql/Activities/SqlCommand.cs | 71 ++++ src/modules/Elsa.Sql/Activities/SqlQuery.cs | 72 +++++ .../Elsa.Sql/Activities/SqlSingleValue.cs | 71 ++++ src/modules/Elsa.Sql/Client/BaseSqlClient.cs | 50 +++ src/modules/Elsa.Sql/Client/ISqlClient.cs | 27 ++ .../Elsa.Sql/Contracts/ISqlClientFactory.cs | 14 + .../Contracts/ISqlClientNamesProvider.cs | 11 + src/modules/Elsa.Sql/Elsa.Sql.csproj | 15 + .../Elsa.Sql/Extensions/ModuleExtensions.cs | 22 ++ .../Elsa.Sql/Factory/SqlClientFactory.cs | 43 +++ src/modules/Elsa.Sql/Features/SqlFeature.cs | 57 ++++ src/modules/Elsa.Sql/FodyWeavers.xml | 3 + .../Implimentations/SqlClientNamesProvider.cs | 23 ++ src/modules/Elsa.Sql/Services/ClientStore.cs | 36 +++ .../UIHints/SqlClientsDropDownProvider.cs | 18 ++ 31 files changed, 1084 insertions(+), 187 deletions(-) create mode 100644 src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj create mode 100644 src/modules/Elsa.Sql.MySql/FodyWeavers.xml create mode 100644 src/modules/Elsa.Sql.MySql/MySqlClient.cs create mode 100644 src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj create mode 100644 src/modules/Elsa.Sql.PostgreSql/FodyWeavers.xml create mode 100644 src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs create mode 100644 src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj create mode 100644 src/modules/Elsa.Sql.SqlServer/FodyWeavers.xml create mode 100644 src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs create mode 100644 src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj create mode 100644 src/modules/Elsa.Sql.Sqlite/FodyWeavers.xml create mode 100644 src/modules/Elsa.Sql.Sqlite/SqliteClient.cs create mode 100644 src/modules/Elsa.Sql/Activities/SqlCommand.cs create mode 100644 src/modules/Elsa.Sql/Activities/SqlQuery.cs create mode 100644 src/modules/Elsa.Sql/Activities/SqlSingleValue.cs create mode 100644 src/modules/Elsa.Sql/Client/BaseSqlClient.cs create mode 100644 src/modules/Elsa.Sql/Client/ISqlClient.cs create mode 100644 src/modules/Elsa.Sql/Contracts/ISqlClientFactory.cs create mode 100644 src/modules/Elsa.Sql/Contracts/ISqlClientNamesProvider.cs create mode 100644 src/modules/Elsa.Sql/Elsa.Sql.csproj create mode 100644 src/modules/Elsa.Sql/Extensions/ModuleExtensions.cs create mode 100644 src/modules/Elsa.Sql/Factory/SqlClientFactory.cs create mode 100644 src/modules/Elsa.Sql/Features/SqlFeature.cs create mode 100644 src/modules/Elsa.Sql/FodyWeavers.xml create mode 100644 src/modules/Elsa.Sql/Implimentations/SqlClientNamesProvider.cs create mode 100644 src/modules/Elsa.Sql/Services/ClientStore.cs create mode 100644 src/modules/Elsa.Sql/UIHints/SqlClientsDropDownProvider.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 5a14c8b2a..408e07ec6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,152 +1,154 @@ - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Elsa.sln b/Elsa.sln index 5f0731b5a..1e29473d9 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -387,6 +387,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Agents.Persistence.Ent EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Kafka", "src\modules\Elsa.Kafka\Elsa.Kafka.csproj", "{BF934627-F531-44FB-BEC2-ECA801FF31E7}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sql", "sql", "{A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql", "src\modules\Elsa.Sql\Elsa.Sql.csproj", "{FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.MySql", "src\modules\Elsa.Sql.MySql\Elsa.Sql.MySql.csproj", "{3BED411B-79B5-4CCC-BD46-9549A427B908}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.PostgreSql", "src\modules\Elsa.Sql.PostgreSql\Elsa.Sql.PostgreSql.csproj", "{6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.Sqlite", "src\modules\Elsa.Sql.Sqlite\Elsa.Sql.Sqlite.csproj", "{FA5E857F-B173-4B5D-8049-B817A210DEF5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.SqlServer", "src\modules\Elsa.Sql.SqlServer\Elsa.Sql.SqlServer.csproj", "{A51F9683-DA9F-45E7-82DE-1E261ACD6D68}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -823,6 +835,26 @@ Global {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF934627-F531-44FB-BEC2-ECA801FF31E7}.Release|Any CPU.Build.0 = Release|Any CPU + {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348}.Release|Any CPU.Build.0 = Release|Any CPU + {3BED411B-79B5-4CCC-BD46-9549A427B908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BED411B-79B5-4CCC-BD46-9549A427B908}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BED411B-79B5-4CCC-BD46-9549A427B908}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BED411B-79B5-4CCC-BD46-9549A427B908}.Release|Any CPU.Build.0 = Release|Any CPU + {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D}.Release|Any CPU.Build.0 = Release|Any CPU + {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA5E857F-B173-4B5D-8049-B817A210DEF5}.Release|Any CPU.Build.0 = Release|Any CPU + {A51F9683-DA9F-45E7-82DE-1E261ACD6D68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -968,6 +1000,12 @@ Global {D5720DBC-8C2B-42D5-9D9F-2FF6EAD4001C} = {2F3E1026-5054-4E1F-899B-F1A7F70F9912} {2B939AC9-03A4-479E-AA0D-CB58F4A7F480} = {50470834-4CD8-479A-8B58-0A1869BA5D37} {BF934627-F531-44FB-BEC2-ECA801FF31E7} = {DD089B8B-DA73-492A-9010-F772D1C178DA} + {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79} + {FD3CD5A8-E9B3-467F-90EB-2B7D5B83F348} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} + {3BED411B-79B5-4CCC-BD46-9549A427B908} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} + {6CC5FBC7-D3D7-4FE3-AD08-C67939BDB24D} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} + {FA5E857F-B173-4B5D-8049-B817A210DEF5} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} + {A51F9683-DA9F-45E7-82DE-1E261ACD6D68} = {A0DC5F8E-5D7F-4E8A-A5DF-B1FC31F7336E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index bf4d53171..506defc69 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -9,9 +9,9 @@ - + - + @@ -19,48 +19,53 @@ + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - - + + diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index fc36b8e1c..ded168c62 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -34,6 +34,11 @@ using Elsa.Server.Web; using Elsa.Server.Web.Extensions; using Elsa.Server.Web.Filters; using Elsa.Server.Web.Messages; +using Elsa.Sql.Extensions; +using Elsa.Sql.MySql; +using Elsa.Sql.PostgreSql; +using Elsa.Sql.Sqlite; +using Elsa.Sql.SqlServer; using Elsa.Tenants.AspNetCore; using Elsa.Tenants.Extensions; using Elsa.Workflows.Api; @@ -340,6 +345,16 @@ services if (useCaching) http.UseCache(); }) + .UseSql(options => + { + options.Clients = client => + { + client.Register("MySql"); + client.Register("PostgreSql"); + client.Register("Sqlite"); + client.Register("Sql Server"); + }; + }) .UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options)) .UseAlterations(alterations => { diff --git a/src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj b/src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj new file mode 100644 index 000000000..54c6148dc --- /dev/null +++ b/src/modules/Elsa.Sql.MySql/Elsa.Sql.MySql.csproj @@ -0,0 +1,18 @@ + + + + + Provides client implementations for interacting with MySql databases. + + elsa module activities sql mysql + + + + + + + + + + + diff --git a/src/modules/Elsa.Sql.MySql/FodyWeavers.xml b/src/modules/Elsa.Sql.MySql/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Sql.MySql/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Sql.MySql/MySqlClient.cs b/src/modules/Elsa.Sql.MySql/MySqlClient.cs new file mode 100644 index 000000000..60d84fd3e --- /dev/null +++ b/src/modules/Elsa.Sql.MySql/MySqlClient.cs @@ -0,0 +1,55 @@ +using MySql.Data.MySqlClient; +using Elsa.Sql.Client; +using System.Data; + +namespace Elsa.Sql.MySql; + +public class MySqlClient : BaseSqlClient, ISqlClient +{ + private string? _connectionString; + + /// + /// MySql client implimentation. + /// + /// + public MySqlClient(string? connectionString) => _connectionString = connectionString; + + /// + /// + /// + public async Task ExecuteCommandAsync(string sqlCommand) + { + using var connection = new MySqlConnection(_connectionString); + connection.Open(); + var command = new MySqlCommand(sqlCommand, connection); + + var result = await command.ExecuteNonQueryAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteScalarAsync(string sqlQuery) + { + using var connection = new MySqlConnection(_connectionString); + connection.Open(); + var command = new MySqlCommand(sqlQuery, connection); + + var result = await command.ExecuteScalarAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteQueryAsync(string sqlQuery) + { + using var connection = new MySqlConnection(_connectionString); + connection.Open(); + var command = new MySqlCommand(sqlQuery, connection); + + using var reader = await command.ExecuteReaderAsync(); + return await Task.FromResult(ReadAsDataSet(reader)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj b/src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj new file mode 100644 index 000000000..82a290850 --- /dev/null +++ b/src/modules/Elsa.Sql.PostgreSql/Elsa.Sql.PostgreSql.csproj @@ -0,0 +1,18 @@ + + + + + Provides client implementations for interacting with PostgreSql databases. + + elsa module activities sql postgresql + + + + + + + + + + + diff --git a/src/modules/Elsa.Sql.PostgreSql/FodyWeavers.xml b/src/modules/Elsa.Sql.PostgreSql/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Sql.PostgreSql/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs b/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs new file mode 100644 index 000000000..495439f30 --- /dev/null +++ b/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs @@ -0,0 +1,55 @@ +using Npgsql; +using Elsa.Sql.Client; +using System.Data; + +namespace Elsa.Sql.PostgreSql; + +public class PostgreSqlClient : BaseSqlClient, ISqlClient +{ + private string? _connectionString; + + /// + /// PostgreSQL client implimentation. + /// + /// + public PostgreSqlClient(string? connectionString) => _connectionString = connectionString; + + /// + /// + /// + public async Task ExecuteCommandAsync(string sqlCommand) + { + using var connection = new NpgsqlConnection(_connectionString); + connection.Open(); + var command = new NpgsqlCommand(sqlCommand, connection); + + var result = await command.ExecuteNonQueryAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteScalarAsync(string sqlQuery) + { + using var connection = new NpgsqlConnection(_connectionString); + connection.Open(); + var command = new NpgsqlCommand(sqlQuery, connection); + + var result = await command.ExecuteScalarAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteQueryAsync(string sqlQuery) + { + using var connection = new NpgsqlConnection(_connectionString); + connection.Open(); + var command = new NpgsqlCommand(sqlQuery, connection); + + using var reader = await command.ExecuteReaderAsync(); + return await Task.FromResult(ReadAsDataSet(reader)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj b/src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj new file mode 100644 index 000000000..cab38ce95 --- /dev/null +++ b/src/modules/Elsa.Sql.SqlServer/Elsa.Sql.SqlServer.csproj @@ -0,0 +1,18 @@ + + + + + Provides client implementations for interacting with Microsoft SQL Server databases. + + elsa module activities sql mssqlserver sqlserver + + + + + + + + + + + diff --git a/src/modules/Elsa.Sql.SqlServer/FodyWeavers.xml b/src/modules/Elsa.Sql.SqlServer/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Sql.SqlServer/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs new file mode 100644 index 000000000..f5b57c263 --- /dev/null +++ b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs @@ -0,0 +1,55 @@ +using System.Data; +using Elsa.Sql.Client; +using Microsoft.Data.SqlClient; + +namespace Elsa.Sql.SqlServer; + +public class SqlServerClient : BaseSqlClient, ISqlClient +{ + private string? _connectionString; + + /// + /// Microsoft SQL server client implimentation. + /// + /// + public SqlServerClient(string? connectionString) => _connectionString = connectionString; + + /// + /// + /// + public async Task ExecuteCommandAsync(string sqlCommand) + { + using var connection = new SqlConnection(_connectionString); + connection.Open(); + var command = new SqlCommand(sqlCommand, connection); + + var result = await command.ExecuteNonQueryAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteScalarAsync(string sqlQuery) + { + using var connection = new SqlConnection(_connectionString); + connection.Open(); + var command = new SqlCommand(sqlQuery, connection); + + var result = await command.ExecuteScalarAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteQueryAsync(string sqlQuery) + { + using var connection = new SqlConnection(_connectionString); + connection.Open(); + var command = new SqlCommand(sqlQuery, connection); + + using var reader = await command.ExecuteReaderAsync(); + return await Task.FromResult(ReadAsDataSet(reader)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj b/src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj new file mode 100644 index 000000000..43bf2bf5a --- /dev/null +++ b/src/modules/Elsa.Sql.Sqlite/Elsa.Sql.Sqlite.csproj @@ -0,0 +1,18 @@ + + + + + Provides client implementations for interacting with Sqlite databases. + + elsa module activities sql sqlite + + + + + + + + + + + diff --git a/src/modules/Elsa.Sql.Sqlite/FodyWeavers.xml b/src/modules/Elsa.Sql.Sqlite/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Sql.Sqlite/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs new file mode 100644 index 000000000..d67e69342 --- /dev/null +++ b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs @@ -0,0 +1,55 @@ +using System.Data; +using Elsa.Sql.Client; +using Microsoft.Data.Sqlite; + +namespace Elsa.Sql.Sqlite; + +public class SqliteClient : BaseSqlClient, ISqlClient +{ + private string? _connectionString; + + /// + /// Sqlite client implimentation. + /// + /// + public SqliteClient(string? connectionString) => _connectionString = connectionString; + + /// + /// + /// + public async Task ExecuteCommandAsync(string sqlCommand) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + var command = new SqliteCommand(sqlCommand, connection); + + var result = await command.ExecuteNonQueryAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteScalarAsync(string sqlQuery) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + var command = new SqliteCommand(sqlQuery, connection); + + var result = await command.ExecuteScalarAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteQueryAsync(string sqlQuery) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + var command = new SqliteCommand(sqlQuery, connection); + + using var reader = await command.ExecuteReaderAsync(); + return await Task.FromResult(ReadAsDataSet(reader)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Activities/SqlCommand.cs b/src/modules/Elsa.Sql/Activities/SqlCommand.cs new file mode 100644 index 000000000..0038e1aeb --- /dev/null +++ b/src/modules/Elsa.Sql/Activities/SqlCommand.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; +using Elsa.Extensions; +using Elsa.Sql.Contracts; +using Elsa.Sql.UIHints; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.UIHints; + +namespace Elsa.Sql.Activities; + +/// +/// Execute given SQL command and returns the number of rows affected. +/// +[Activity("Elsa", "SQL", "Execute given SQL command and returns the number of rows affected.", DisplayName = "SQL Command", Kind = ActivityKind.Task)] +public class SqlCommand : Activity +{ + /// + /// + /// + public SqlCommand([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base (source, line) + { + } + + /// + /// Database client to connect with. + /// + [Input( + Description = "Database client.", + UIHint = InputUIHints.DropDown, + UIHandler = typeof(SqlClientsDropDownProvider))] + public Input Client { get; set; } = default!; + + /// + /// Connection string. + /// + [Input( + Description = "Connection string.", + CanContainSecrets = true)] + public Input ConnectionString { get; set; } = default!; + + /// + /// Command to run against the database. + /// + [Input( + Description = "Command to run against the database.", + UIHint = InputUIHints.SqlEditor)] + public Input Command { get; set; } = default!; + + + /// + /// The number of affected rows. + /// + [Output( + Description = "The number of rows affected.")] + public Output Result { get; set; } = default!; + + /// + /// + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var factory = context.GetRequiredService(); + var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context)); + + var result = await client.ExecuteCommandAsync(Command.GetOrDefault(context)); + context.Set(Result, result); + + await CompleteAsync(context); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Activities/SqlQuery.cs b/src/modules/Elsa.Sql/Activities/SqlQuery.cs new file mode 100644 index 000000000..c1d922149 --- /dev/null +++ b/src/modules/Elsa.Sql/Activities/SqlQuery.cs @@ -0,0 +1,72 @@ +using System.Data; +using System.Runtime.CompilerServices; +using Elsa.Extensions; +using Elsa.Sql.Contracts; +using Elsa.Sql.UIHints; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.UIHints; + +namespace Elsa.Sql.Activities; + +/// +/// Execute given SQL query and return the resulting data. +/// +[Activity("Elsa", "SQL", "Execute given SQL query and return the resulting data.", DisplayName = "SQL Query", Kind = ActivityKind.Task)] +public class SqlQuery : Activity +{ + /// + /// + /// + public SqlQuery([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// Database client to connect with. + /// + [Input( + Description = "Database client.", + UIHint = InputUIHints.DropDown, + UIHandler = typeof(SqlClientsDropDownProvider))] + public Input Client { get; set; } = default!; + + /// + /// Connection string. + /// + [Input( + Description = "Connection string.", + CanContainSecrets = true)] + public Input ConnectionString { get; set; } = default!; + + /// + /// Query to run against the database. + /// + [Input( + Description = "Query to run against the database.", + UIHint = InputUIHints.SqlEditor)] + public Input Query { get; set; } = default!; + + + /// + /// of queried results. + /// + [Output( + Description = "DataSet of queried results.")] + public Output Results { get; set; } = default!; + + /// + /// + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var factory = context.GetRequiredService(); + var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context)); + + var results = await client.ExecuteQueryAsync(Query.GetOrDefault(context)); + context.Set(Results, results); + + await CompleteAsync(context); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Activities/SqlSingleValue.cs b/src/modules/Elsa.Sql/Activities/SqlSingleValue.cs new file mode 100644 index 000000000..037863911 --- /dev/null +++ b/src/modules/Elsa.Sql/Activities/SqlSingleValue.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; +using Elsa.Extensions; +using Elsa.Sql.Contracts; +using Elsa.Sql.UIHints; +using Elsa.Workflows; +using Elsa.Workflows.Attributes; +using Elsa.Workflows.Models; +using Elsa.Workflows.UIHints; + +namespace Elsa.Sql.Activities; + +/// +/// Execute given SQL command and return a single result. +/// +[Activity("Elsa", "SQL", "Execute given SQL command and return a single result.", DisplayName = "SQL Single Value", Kind = ActivityKind.Task)] +public class SqlSingleValue : Activity +{ + /// + /// + /// + public SqlSingleValue([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// Database client to connect with. + /// + [Input( + Description = "Database client.", + UIHint = InputUIHints.DropDown, + UIHandler = typeof(SqlClientsDropDownProvider))] + public Input Client { get; set; } = default!; + + /// + /// Connection string. + /// + [Input( + Description = "Connection string.", + CanContainSecrets = true)] + public Input ConnectionString { get; set; } = default!; + + /// + /// Query to run against the database. + /// + [Input( + Description = "Query to run against the database.", + UIHint = InputUIHints.SqlEditor)] + public Input Query { get; set; } = default!; + + + /// + /// Command result. + /// + [Output( + Description = "Command result.")] + public Output Result { get; set; } = default!; + + /// + /// + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var factory = context.GetRequiredService(); + var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context)); + + var result = await client.ExecuteScalarAsync(Query.GetOrDefault(context)); + context.Set(Result, result); + + await CompleteAsync(context); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs new file mode 100644 index 000000000..0298d5493 --- /dev/null +++ b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs @@ -0,0 +1,50 @@ +using System.Data; + +namespace Elsa.Sql.Client; + +public abstract class BaseSqlClient +{ + /// + /// Returns data as a . + /// + /// Reader to return data from. + /// of data. + protected static DataSet ReadAsDataSet(IDataReader reader) + { + var dataSet = new DataSet("dataset"); + + var schematable = reader.GetSchemaTable(); + var data = new DataSet(); + dataSet.Tables.Add(ReadAsDataTable(reader)); + + return dataSet; + } + + /// + /// Returns data as a . + /// + /// Reader to return data from. + /// of data. + protected static DataTable ReadAsDataTable(IDataReader reader) + { + var data = new DataTable(); + var schemaTable =reader.GetSchemaTable(); + + foreach (DataRow row in schemaTable.Rows) + { + string colName = row.Field("ColumnName"); + Type t = row.Field("DataType"); + data.Columns.Add(colName, t); + } + + while (reader.Read()) + { + var newRow = data.Rows.Add(); + foreach (DataColumn col in data.Columns) + { + newRow[col.ColumnName] = reader[col.ColumnName]; + } + } + return data; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Client/ISqlClient.cs b/src/modules/Elsa.Sql/Client/ISqlClient.cs new file mode 100644 index 000000000..8e4e3fe43 --- /dev/null +++ b/src/modules/Elsa.Sql/Client/ISqlClient.cs @@ -0,0 +1,27 @@ +using System.Data; + +namespace Elsa.Sql.Client; + +public interface ISqlClient +{ + /// + /// Asyncronously executes a Transact-SQL statement against the connection and returns the number of rows affected. + /// + /// The command to execute + /// The number of rows affected. + public Task ExecuteCommandAsync(string sqlCommand); + + /// + /// Asyncronously executes the query, and returns the first column of the first row in the result set returned by the query. Additional columns or rows are ignored. + /// + /// The query to execute + /// The first column of the first row in the result set, or a null reference if the result set is empty. Returns a maximum of 2033 characters. + public Task ExecuteScalarAsync(string sqlQuery); + + /// + /// Asyncronously executes the query, and returns a dataset of data returned by the query. + /// + /// Query to execute + /// DataSet of the quiried data + public Task ExecuteQueryAsync(string sqlQuery); +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Contracts/ISqlClientFactory.cs b/src/modules/Elsa.Sql/Contracts/ISqlClientFactory.cs new file mode 100644 index 000000000..de2d7771c --- /dev/null +++ b/src/modules/Elsa.Sql/Contracts/ISqlClientFactory.cs @@ -0,0 +1,14 @@ +using Elsa.Sql.Client; + +namespace Elsa.Sql.Contracts; + +public interface ISqlClientFactory +{ + /// + /// Create an instance of the registered client. + /// + /// The name of the registered client to create. This can either be clientName used during registration or the default nameof(client) itself. + /// Connection string. + /// + public ISqlClient CreateClient(string clientName, string connectionString); +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Contracts/ISqlClientNamesProvider.cs b/src/modules/Elsa.Sql/Contracts/ISqlClientNamesProvider.cs new file mode 100644 index 000000000..b1b48eef4 --- /dev/null +++ b/src/modules/Elsa.Sql/Contracts/ISqlClientNamesProvider.cs @@ -0,0 +1,11 @@ +namespace Elsa.Sql.Contracts; + +public interface ISqlClientNamesProvider +{ + /// + /// Returns a dictionary of registered clients. + /// + /// A token to monitor cancellation requests. + /// A of registered client names their . + Task> GetRegisteredSqlClientNamesAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Elsa.Sql.csproj b/src/modules/Elsa.Sql/Elsa.Sql.csproj new file mode 100644 index 000000000..bfb24be1e --- /dev/null +++ b/src/modules/Elsa.Sql/Elsa.Sql.csproj @@ -0,0 +1,15 @@ + + + + + Provides activities to interact with sql databases. + + elsa module activities sql + + + + + + + + diff --git a/src/modules/Elsa.Sql/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Sql/Extensions/ModuleExtensions.cs new file mode 100644 index 000000000..de787d172 --- /dev/null +++ b/src/modules/Elsa.Sql/Extensions/ModuleExtensions.cs @@ -0,0 +1,22 @@ +using Elsa.Features.Services; +using Elsa.Sql.Features; + +namespace Elsa.Sql.Extensions; + +/// +/// Provides methods to install and configure SQL client features. +/// +public static class ModuleExtensions +{ + /// + /// Adds the feature to the system. + /// + /// + /// + /// + public static IModule UseSql(this IModule configuration, Action? configure= default) + { + configuration.Configure(configure); + return configuration; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Factory/SqlClientFactory.cs b/src/modules/Elsa.Sql/Factory/SqlClientFactory.cs new file mode 100644 index 000000000..4422b099b --- /dev/null +++ b/src/modules/Elsa.Sql/Factory/SqlClientFactory.cs @@ -0,0 +1,43 @@ +using Elsa.Sql.Client; +using Elsa.Sql.Contracts; +using Elsa.Sql.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Sql.Factory; + +/// +/// SQL client factory +/// +public class SqlClientFactory : ISqlClientFactory +{ + private readonly IServiceProvider _serviceProvider; + + public SqlClientFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + /// + /// + /// + public ISqlClient CreateClient(string clientName, string connectionString) + { + if (string.IsNullOrEmpty(clientName)) + { + throw new ArgumentException($"Client name can not be empty or null.", nameof(clientName)); + } + if (string.IsNullOrEmpty(connectionString)) + { + throw new ArgumentException($"Connection string can not be empty or null.", nameof(connectionString)); + } + if (_serviceProvider.GetRequiredService().Clients.TryGetValue(clientName, out var clientType)) + { + try + { + return ActivatorUtilities.CreateInstance(_serviceProvider, clientType, connectionString) as ISqlClient; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Unable to create instance of '{clientName}' of type '{clientType}'.", ex); + } + } + throw new ArgumentException($"No registered SQL client provider for '{clientName}'."); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Features/SqlFeature.cs b/src/modules/Elsa.Sql/Features/SqlFeature.cs new file mode 100644 index 000000000..b0e4cbab0 --- /dev/null +++ b/src/modules/Elsa.Sql/Features/SqlFeature.cs @@ -0,0 +1,57 @@ +using Elsa.Extensions; +using Elsa.Features.Abstractions; +using Elsa.Features.Services; +using Elsa.Sql.Contracts; +using Elsa.Sql.Factory; +using Elsa.Sql.Implimentations; +using Elsa.Sql.Services; +using Elsa.Sql.UIHints; +using Elsa.Workflows; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Sql.Features; + +/// +/// Setup SQL client features +/// +public class SqlFeature : FeatureBase +{ + /// + /// Set a callback to configure . + /// + public Action Clients { get; set; } = _ => { }; + + /// + /// + /// + /// + public SqlFeature(IModule module) : base(module) + { + } + + /// + /// + /// + public override void Configure() + { + Module.AddActivitiesFrom(); + } + + /// + /// + /// + public override void Apply() + { + Services + .AddSingleton(provider => + { + ClientStore clientRegistry = new(); + Clients.Invoke(clientRegistry); + return clientRegistry; + }) + .AddSingleton() + + .AddScoped() + .AddScoped(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/FodyWeavers.xml b/src/modules/Elsa.Sql/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/src/modules/Elsa.Sql/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Implimentations/SqlClientNamesProvider.cs b/src/modules/Elsa.Sql/Implimentations/SqlClientNamesProvider.cs new file mode 100644 index 000000000..5c2cba45c --- /dev/null +++ b/src/modules/Elsa.Sql/Implimentations/SqlClientNamesProvider.cs @@ -0,0 +1,23 @@ +using Elsa.Sql.Contracts; +using Elsa.Sql.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Sql.Implimentations; + +/// +/// Returns registered client names +/// +public class SqlClientNamesProvider : ISqlClientNamesProvider +{ + private readonly IServiceProvider _serviceProvider; + + public SqlClientNamesProvider(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + /// + /// + /// + public Task> GetRegisteredSqlClientNamesAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_serviceProvider.GetRequiredService().Clients); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Services/ClientStore.cs b/src/modules/Elsa.Sql/Services/ClientStore.cs new file mode 100644 index 000000000..d76727aea --- /dev/null +++ b/src/modules/Elsa.Sql/Services/ClientStore.cs @@ -0,0 +1,36 @@ +using Elsa.Sql.Client; + +namespace Elsa.Sql.Services; +public class ClientStore +{ + private readonly Dictionary clients = new(); + + /// + /// Dictionary of registered clients and their type. + /// + public IReadOnlyDictionary Clients => clients; + + /// + /// Registers the specified client type with the store. + /// The client type must inherit from . + /// + /// + /// The type of the client to be registered. The client must be a class that implements the interface. + /// + /// + /// The name of the client to register. If not provided, the name defaults to nameof(TClient). + /// This value is used as a key to identify the client in the store. + /// + /// + /// Thrown when a client with the same name is already registered in the store. + /// + /// + /// This method registers a client type to the store using a unique key. The key is either the provided or the default name derived from . + /// + public void Register(string? name) where TClient : class, ISqlClient + { + var key = string.IsNullOrEmpty(name) ? nameof(TClient) : name; + if (clients.ContainsKey(key)) { throw new InvalidOperationException($"Client with key '{name}' is already registered."); } + clients.Add(key, typeof(TClient)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/UIHints/SqlClientsDropDownProvider.cs b/src/modules/Elsa.Sql/UIHints/SqlClientsDropDownProvider.cs new file mode 100644 index 000000000..019271df6 --- /dev/null +++ b/src/modules/Elsa.Sql/UIHints/SqlClientsDropDownProvider.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using Elsa.Sql.Contracts; +using Elsa.Workflows.UIHints.Dropdown; + +namespace Elsa.Sql.UIHints; + +/// +/// Provides registered clients for the Client input field. +/// +/// +public class SqlClientsDropDownProvider(ISqlClientNamesProvider sqlClientNamesProvider) : DropDownOptionsProviderBase +{ + protected override async ValueTask> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken) + { + var clients = await sqlClientNamesProvider.GetRegisteredSqlClientNamesAsync(cancellationToken); + return clients.Select(x => new SelectListItem(x.Key, x.Key)).ToList(); + } +} \ No newline at end of file