Add project files.
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
using System.Reflection;
|
||||
using Elsa.Workflows.UIHints.CodeEditor;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Sql.Activities;
|
||||
|
||||
internal class SqlCodeOptionsProvider : CodeEditorOptionsProviderBase
|
||||
{
|
||||
protected override string GetLanguage(PropertyInfo propertyInfo, object? context) => "sql";
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Expressions.Models;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Execute given SQL command and returns the number of rows affected.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "SQL", "Execute given SQL command and returns the number of rows affected.", DisplayName = "SQL Command", Kind = ActivityKind.Task)]
|
||||
public class SqlCommand : Activity
|
||||
{
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public SqlCommand([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Database client to connect with.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "Database client.",
|
||||
UIHint = InputUIHints.DropDown,
|
||||
UIHandler = typeof(SqlClientsDropDownProvider))]
|
||||
public Input<string?> Client { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Connection string.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "Connection string.",
|
||||
CanContainSecrets = true)]
|
||||
public Input<string?> ConnectionString { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Command to run against the database.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "Command to run against the database.",
|
||||
DefaultSyntax = "Sql",
|
||||
UIHint = InputUIHints.CodeEditor,
|
||||
UIHandler = typeof(SqlCodeOptionsProvider)
|
||||
)]
|
||||
public Input<string?> Command { get; set; } = null!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The number of affected rows.
|
||||
/// </summary>
|
||||
[Output(
|
||||
Description = "The number of rows affected.")]
|
||||
public Output<int?> Result { get; set; } = null!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var command = Command.GetOrDefault(context);
|
||||
|
||||
// If no command was specified, there's nothing to do.
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
return;
|
||||
|
||||
// Get and execute the SQL evaluator.
|
||||
var evaluator = context.GetRequiredService<ISqlEvaluator>();
|
||||
var evaluatedQuery = await evaluator.EvaluateAsync(command, context.ExpressionExecutionContext, new ExpressionEvaluatorOptions(), context.CancellationToken);
|
||||
|
||||
// Create client
|
||||
var factory = context.GetRequiredService<ISqlClientFactory>();
|
||||
var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context));
|
||||
|
||||
// Execute command
|
||||
var result = await client.ExecuteCommandAsync(evaluatedQuery);
|
||||
context.Set(Result, result);
|
||||
|
||||
await CompleteAsync(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Data;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Expressions.Models;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Execute given SQL query and return the resulting data.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "SQL", "Execute given SQL query and return the resulting data.", DisplayName = "SQL Query", Kind = ActivityKind.Task)]
|
||||
public class SqlQuery : Activity
|
||||
{
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public SqlQuery([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Database client to connect with.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "Database client.",
|
||||
UIHint = InputUIHints.DropDown,
|
||||
UIHandler = typeof(SqlClientsDropDownProvider))]
|
||||
public Input<string?> Client { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Connection string.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "Connection string.",
|
||||
CanContainSecrets = true)]
|
||||
public Input<string?> ConnectionString { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Query to run against the database.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "Query to run against the database.",
|
||||
DefaultSyntax = "Sql",
|
||||
UIHint = InputUIHints.CodeEditor,
|
||||
UIHandler = typeof(SqlCodeOptionsProvider)
|
||||
)]
|
||||
public Input<string?> Query { get; set; } = null!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DataSet"/> of queried results.
|
||||
/// </summary>
|
||||
[Output(
|
||||
Description = "DataSet of queried results.",
|
||||
IsSerializable = false)]
|
||||
public Output<DataSet?> Results { get; set; } = null!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var query = Query.GetOrDefault(context);
|
||||
|
||||
// If no query was specified, there's nothing to do.
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
return;
|
||||
|
||||
// Get and execute the SQL evaluator.
|
||||
var evaluator = context.GetRequiredService<ISqlEvaluator>();
|
||||
var evaluatedQuery = await evaluator.EvaluateAsync(query, context.ExpressionExecutionContext, new ExpressionEvaluatorOptions(), context.CancellationToken);
|
||||
|
||||
// Create client
|
||||
var factory = context.GetRequiredService<ISqlClientFactory>();
|
||||
var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context));
|
||||
|
||||
// Execute query
|
||||
var results = await client.ExecuteQueryAsync(evaluatedQuery);
|
||||
context.Set(Results, results);
|
||||
|
||||
await CompleteAsync(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Expressions.Models;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Execute given SQL command and return a single result.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "SQL", "Execute given SQL command and return a single result.", DisplayName = "SQL Single Value", Kind = ActivityKind.Task)]
|
||||
public class SqlSingleValue : Activity
|
||||
{
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public SqlSingleValue([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Database client to connect with.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "Database client.",
|
||||
UIHint = InputUIHints.DropDown,
|
||||
UIHandler = typeof(SqlClientsDropDownProvider))]
|
||||
public Input<string?> Client { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Connection string.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "Connection string.",
|
||||
CanContainSecrets = true)]
|
||||
public Input<string?> ConnectionString { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Command to run against the database.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "Query to run against the database.",
|
||||
DefaultSyntax = "Sql",
|
||||
UIHint = InputUIHints.CodeEditor,
|
||||
UIHandler = typeof(SqlCodeOptionsProvider)
|
||||
)]
|
||||
public Input<string?> Command { get; set; } = null!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Command result.
|
||||
/// </summary>
|
||||
[Output(
|
||||
Description = "Command result.")]
|
||||
public Output<object?> Result { get; set; } = null!;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var command = Command.GetOrDefault(context);
|
||||
|
||||
// If no command was specified, there's nothing to do.
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
return;
|
||||
|
||||
// Get and execute the SQL evaluator.
|
||||
var evaluator = context.GetRequiredService<ISqlEvaluator>();
|
||||
var evaluatedQuery = await evaluator.EvaluateAsync(command, context.ExpressionExecutionContext, new ExpressionEvaluatorOptions(), context.CancellationToken);
|
||||
|
||||
// Create client
|
||||
var factory = context.GetRequiredService<ISqlClientFactory>();
|
||||
var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context));
|
||||
|
||||
// Execute command
|
||||
var result = await client.ExecuteScalarAsync(evaluatedQuery);
|
||||
context.Set(Result, result);
|
||||
|
||||
await CompleteAsync(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using Elsa.Sql.Models;
|
||||
|
||||
namespace Elsa.Sql.Client;
|
||||
|
||||
public abstract class BaseSqlClient : ISqlClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The connection string used to connect with the database.
|
||||
/// </summary>
|
||||
protected readonly string _connectionString;
|
||||
|
||||
/// <summary>
|
||||
/// The marker used when injecting parameters into a query.
|
||||
/// Default: "@"
|
||||
/// </summary>
|
||||
public virtual string ParameterMarker { get; set; } = "@";
|
||||
|
||||
/// <summary>
|
||||
/// The text following the <c>ParameterMarker</c> when injecting parameters into a query.
|
||||
/// Default: "param"
|
||||
/// </summary>
|
||||
public virtual string ParameterText { get; set; } = "p";
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to add a counter to the end of the parameter string.
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
public virtual bool IncrementParameter { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Default base implementation for an SQL client.
|
||||
/// </summary>
|
||||
/// <param name="connectionString"></param>
|
||||
protected BaseSqlClient(string connectionString) => _connectionString = connectionString;
|
||||
|
||||
/// <summary>
|
||||
/// Create a connection using the client specific connection.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected abstract DbConnection CreateConnection();
|
||||
|
||||
/// <summary>
|
||||
/// Create a command using the client specific connection.
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
/// <param name="connection"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract DbCommand CreateCommand(string query, DbConnection connection);
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public async Task<int?> ExecuteCommandAsync(EvaluatedQuery evaluatedQuery)
|
||||
{
|
||||
using var connection = CreateConnection();
|
||||
connection.Open();
|
||||
var command = CreateCommand(evaluatedQuery.Query, connection);
|
||||
AddCommandParameters(command, evaluatedQuery.Parameters);
|
||||
|
||||
var result = await command.ExecuteNonQueryAsync();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public async Task<object?> ExecuteScalarAsync(EvaluatedQuery evaluatedQuery)
|
||||
{
|
||||
using var connection = CreateConnection();
|
||||
connection.Open();
|
||||
var command = CreateCommand(evaluatedQuery.Query, connection);
|
||||
AddCommandParameters(command, evaluatedQuery.Parameters);
|
||||
|
||||
var result = await command.ExecuteScalarAsync();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public async Task<DataSet?> ExecuteQueryAsync(EvaluatedQuery evaluatedQuery)
|
||||
{
|
||||
using var connection = CreateConnection();
|
||||
connection.Open();
|
||||
var command = CreateCommand(evaluatedQuery.Query, connection);
|
||||
AddCommandParameters(command, evaluatedQuery.Parameters);
|
||||
|
||||
using var reader = await command.ExecuteReaderAsync();
|
||||
return await Task.FromResult(ReadAsDataSet(reader));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add parameters into the query to prevent SQL injection.
|
||||
/// </summary>
|
||||
/// <param name="command">Command to add the parameters to</param>
|
||||
/// <param name="parameters">Parameters to add</param>
|
||||
/// <returns></returns>
|
||||
private DbCommand AddCommandParameters(DbCommand command, Dictionary<string, object?> parameters)
|
||||
{
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
var dbParam = command.CreateParameter();
|
||||
dbParam.ParameterName = param.Key;
|
||||
dbParam.Value = param.Value ?? DBNull.Value;
|
||||
command.Parameters.Add(dbParam);
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="IDataReader"/> data as a <see cref="DataSet"/>.
|
||||
/// </summary>
|
||||
/// <param name="reader">Reader to return data from.</param>
|
||||
/// <returns><see cref="DataSet"/> of data.</returns>
|
||||
private DataSet ReadAsDataSet(IDataReader reader)
|
||||
{
|
||||
var dataSet = new DataSet("dataset");
|
||||
dataSet.Tables.Add(ReadAsDataTable(reader));
|
||||
return dataSet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see cref="IDataReader"/> data as a <see cref="DataTable"/>.
|
||||
/// </summary>
|
||||
/// <param name="reader">Reader to return data from.</param>
|
||||
/// <returns><see cref="DataTable"/> of data.</returns>
|
||||
private DataTable ReadAsDataTable(IDataReader reader)
|
||||
{
|
||||
var data = new DataTable();
|
||||
var schemaTable =reader.GetSchemaTable();
|
||||
|
||||
foreach (DataRow row in schemaTable.Rows)
|
||||
{
|
||||
string colName = row.Field<string>("ColumnName");
|
||||
Type t = row.Field<Type>("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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Data;
|
||||
using Elsa.Sql.Models;
|
||||
|
||||
namespace Elsa.Sql.Client;
|
||||
|
||||
public interface ISqlClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The marker used when injecting parameters into a query.
|
||||
/// </summary>
|
||||
public string ParameterMarker { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The text following the <c>ParameterMarker</c> when injecting parameters into a query.
|
||||
/// </summary>
|
||||
public string ParameterText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to add a counter to the end of the parameter string.
|
||||
/// </summary>
|
||||
public bool IncrementParameter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously executes a Transact-SQL statement against the connection and returns the number of rows affected.
|
||||
/// </summary>
|
||||
/// <param name="evaluatedQuery">The evaluated query to execute.</param>
|
||||
/// <returns>The number of rows affected.</returns>
|
||||
public Task<int?> ExecuteCommandAsync(EvaluatedQuery evaluatedQuery);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously 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.
|
||||
/// </summary>
|
||||
/// <param name="evaluatedQuery">The evaluated query to execute.</param>
|
||||
/// <returns>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.</returns>
|
||||
public Task<object?> ExecuteScalarAsync(EvaluatedQuery evaluatedQuery);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously executes the query, and returns a dataset of data returned by the query.
|
||||
/// </summary>
|
||||
/// <param name="evaluatedQuery">The evaluated query to execute.</param>
|
||||
/// <returns>DataSet of the queried data</returns>
|
||||
public Task<DataSet?> ExecuteQueryAsync(EvaluatedQuery evaluatedQuery);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Elsa.Sql.Client;
|
||||
|
||||
namespace Elsa.Sql.Contracts;
|
||||
|
||||
public interface ISqlClientFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create an instance of the registered client.
|
||||
/// </summary>
|
||||
/// <param name="clientName">The name of the registered client to create. This can either be clientName used during registration or the default nameof(client) itself.</param>
|
||||
/// <param name="connectionString">Connection string.</param>
|
||||
/// <returns></returns>
|
||||
public ISqlClient CreateClient(string clientName, string connectionString);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Elsa.Sql.Contracts;
|
||||
|
||||
public interface ISqlClientNamesProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a dictionary of registered clients.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A token to monitor cancellation requests.</param>
|
||||
/// <returns>A <see cref="Dictionary{TKey, TValue}"/> of registered client names their <see cref="Type"/>.</returns>
|
||||
Task<IReadOnlyDictionary<string, Type>> GetRegisteredSqlClientNamesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Sql.Models;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Sql.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates SQL expressions.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public interface ISqlEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates a SQL expression.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to evaluate.</param>
|
||||
/// <param name="context">The context in which the expression is evaluated.</param>
|
||||
/// <param name="options">A set of options.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
/// <returns>The <see cref="EvaluatedQuery"/> result.</returns>
|
||||
Task<EvaluatedQuery> EvaluateAsync(
|
||||
string expression,
|
||||
ExpressionExecutionContext context,
|
||||
ExpressionEvaluatorOptions options,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides activities to interact with sql databases.
|
||||
</Description>
|
||||
<PackageTags>elsa module activities sql</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,29 @@
|
||||
using Elsa.Sql.Contracts;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Expressions.Models;
|
||||
|
||||
namespace Elsa.Sql.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates SQL expressions.
|
||||
/// </summary>
|
||||
public class SqlExpressionHandler : IExpressionHandler
|
||||
{
|
||||
private readonly ISqlEvaluator _sqlEvaluator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlExpressionHandler"/> class.
|
||||
/// </summary>
|
||||
public SqlExpressionHandler(ISqlEvaluator sqlEvaluator)
|
||||
{
|
||||
_sqlEvaluator = sqlEvaluator;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<object?> EvaluateAsync(Expression expression, Type returnType, ExpressionExecutionContext context, ExpressionEvaluatorOptions options)
|
||||
{
|
||||
var script = expression.Value.ConvertTo<string>() ?? "";
|
||||
return await _sqlEvaluator.EvaluateAsync(script, context, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Sql.Features;
|
||||
|
||||
namespace Elsa.Sql.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods to install and configure SQL client features.
|
||||
/// </summary>
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the <see cref="SqlFeature"/> feature to the system.
|
||||
/// </summary>
|
||||
/// <param name="configuration"></param>
|
||||
/// <param name="configure"></param>
|
||||
/// <returns></returns>
|
||||
public static IModule UseSql(this IModule configuration, Action<SqlFeature>? configure = default)
|
||||
{
|
||||
configuration.Configure(configure);
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Elsa.Sql.Client;
|
||||
using Elsa.Sql.Contracts;
|
||||
using Elsa.Sql.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Sql.Factory;
|
||||
|
||||
/// <summary>
|
||||
/// SQL client factory
|
||||
/// </summary>
|
||||
public class SqlClientFactory : ISqlClientFactory
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public SqlClientFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
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<ClientStore>().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}'.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Sql.Activities;
|
||||
using Elsa.Sql.Contracts;
|
||||
using Elsa.Sql.Factory;
|
||||
using Elsa.Sql.Providers;
|
||||
using Elsa.Sql.Services;
|
||||
using Elsa.Sql.UIHints;
|
||||
using Elsa.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Sql.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Setup SQL client features
|
||||
/// </summary>
|
||||
public class SqlFeature : FeatureBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Set a callback to configure <see cref="ClientStore"/>.
|
||||
/// </summary>
|
||||
public Action<ClientStore> Clients { get; set; } = _ => { };
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
/// <param name="module"></param>
|
||||
public SqlFeature(IModule module) : base(module)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public override void Configure()
|
||||
{
|
||||
Module.AddActivitiesFrom<SqlFeature>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public override void Apply()
|
||||
{
|
||||
Services
|
||||
// Services
|
||||
.AddSingleton(provider =>
|
||||
{
|
||||
ClientStore clientRegistry = new();
|
||||
Clients.Invoke(clientRegistry);
|
||||
return clientRegistry;
|
||||
})
|
||||
.AddSingleton<ISqlClientFactory, SqlClientFactory>()
|
||||
.AddScoped<ISqlEvaluator, SqlEvaluator>()
|
||||
|
||||
// Providers
|
||||
.AddExpressionDescriptorProvider<SqlExpressionDescriptorProvider>()
|
||||
.AddScoped<IPropertyUIHandler, SqlCodeOptionsProvider>()
|
||||
.AddScoped<IPropertyUIHandler, SqlClientsDropDownProvider>()
|
||||
.AddScoped<ISqlClientNamesProvider, SqlClientNamesProvider>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace Elsa.Sql.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a safely formatted SQL expression result.
|
||||
/// </summary>
|
||||
public class EvaluatedQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Query with parameterized values
|
||||
/// </summary>
|
||||
public string Query { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to inject into the query at execution
|
||||
/// </summary>
|
||||
public Dictionary<string, object?> Parameters { get; set; } = new Dictionary<string, object?>();
|
||||
|
||||
/// <summary>
|
||||
/// An evaluated query response.
|
||||
/// </summary>
|
||||
/// <param name="query">The evaluated query</param>
|
||||
public EvaluatedQuery(string query) => Query = query;
|
||||
|
||||
/// <summary>
|
||||
/// An evaluated query response.
|
||||
/// </summary>
|
||||
/// <param name="query">The evaluated query</param>
|
||||
/// <param name="parameters">Parameters to pass into the parameterized query</param>
|
||||
public EvaluatedQuery(string query, Dictionary<string, object?> parameters)
|
||||
{
|
||||
Query = query;
|
||||
Parameters = parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Elsa.Sql.Contracts;
|
||||
using Elsa.Sql.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Sql.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Returns registered client names
|
||||
/// </summary>
|
||||
public class SqlClientNamesProvider : ISqlClientNamesProvider
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public SqlClientNamesProvider(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc/>
|
||||
/// </summary>
|
||||
public Task<IReadOnlyDictionary<string, Type>> GetRegisteredSqlClientNamesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(_serviceProvider.GetRequiredService<ClientStore>().Clients);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Sql.Expressions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Sql.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides SQL expression descriptors.
|
||||
/// </summary>
|
||||
public class SqlExpressionDescriptorProvider : IExpressionDescriptorProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the expression type.
|
||||
/// </summary>
|
||||
private const string TypeName = "Sql";
|
||||
|
||||
public IEnumerable<ExpressionDescriptor> GetDescriptors()
|
||||
{
|
||||
yield return new()
|
||||
{
|
||||
Type = TypeName,
|
||||
DisplayName = "SQL",
|
||||
Properties = new { MonacoLanguage = "sql" }.ToDictionary(),
|
||||
HandlerFactory = ActivatorUtilities.GetServiceOrCreateInstance<SqlExpressionHandler>
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Elsa.Sql.Client;
|
||||
|
||||
namespace Elsa.Sql.Services;
|
||||
public class ClientStore
|
||||
{
|
||||
private readonly Dictionary<string, Type> clients = new();
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary of registered clients and their type.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, Type> Clients => clients;
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified client type <typeparamref name="TClient"/> with the store.
|
||||
/// The client type must inherit from <see cref="ISqlClient"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TClient">
|
||||
/// The type of the client to be registered. The client must be a class that implements the <see cref="ISqlClient"/> interface.
|
||||
/// </typeparam>
|
||||
/// <param name="name">
|
||||
/// The name of the client to register. If not provided, the name defaults to <c>nameof(TClient)</c>.
|
||||
/// This value is used as a key to identify the client in the store.
|
||||
/// </param>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when a client with the same name is already registered in the store.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// This method registers a client type to the store using a unique key. The key is either the provided <paramref name="name"/> or the default name derived from <typeparamref name="TClient"/>.
|
||||
/// </remarks>
|
||||
public void Register<TClient>(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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System.Text;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Sql.Contracts;
|
||||
using Elsa.Sql.Models;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.Sql.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A SQL expression evaluator.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="SqlEvaluator"/> class.WS
|
||||
/// </remarks>
|
||||
public class SqlEvaluator() : ISqlEvaluator
|
||||
{
|
||||
private WorkflowExecutionContext executionContext;
|
||||
private ActivityExecutionContext activityContext;
|
||||
private ExpressionExecutionContext expressionContext;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<EvaluatedQuery> EvaluateAsync(
|
||||
string expression,
|
||||
ExpressionExecutionContext context,
|
||||
ExpressionEvaluatorOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!expression.Contains("{{")) return new EvaluatedQuery(expression);
|
||||
|
||||
expressionContext = context;
|
||||
executionContext = context.GetWorkflowExecutionContext();
|
||||
activityContext = context.GetActivityExecutionContext();
|
||||
|
||||
// Create client
|
||||
var factory = context.GetRequiredService<ISqlClientFactory>();
|
||||
var client = factory.CreateClient(activityContext.ActivityState["Client"].ToString(), activityContext.ActivityState["ConnectionString"].ToString());
|
||||
|
||||
var sb = new StringBuilder();
|
||||
int start = 0;
|
||||
var parameters = new Dictionary<string, object?>();
|
||||
int paramIndex = 0;
|
||||
|
||||
while (start < expression.Length)
|
||||
{
|
||||
int openIndex = expression.IndexOf("{{", start);
|
||||
if (openIndex == -1)
|
||||
{
|
||||
sb.Append(expression.AsSpan(start));
|
||||
break;
|
||||
}
|
||||
|
||||
// Append everything before {{
|
||||
sb.Append(expression.AsSpan(start, openIndex - start));
|
||||
|
||||
// Find the closing }}
|
||||
int closeIndex = expression.IndexOf("}}", openIndex + 2);
|
||||
if (closeIndex == -1) throw new FormatException("Unmatched '{{' found in SQL expression.");
|
||||
|
||||
// Extract key
|
||||
string key = expression.Substring(openIndex + 2, closeIndex - openIndex - 2).Trim();
|
||||
if (string.IsNullOrEmpty(key)) throw new FormatException("Empty placeholder '{{}}' is not allowed.");
|
||||
|
||||
// Resolve value and replace with parameterized name
|
||||
var counterValue = client.IncrementParameter ? paramIndex++.ToString() : string.Empty;
|
||||
string paramName = $"{client.ParameterMarker}{client.ParameterText}{counterValue}";
|
||||
parameters[paramName] = ResolveValue(key);
|
||||
|
||||
sb.Append(paramName);
|
||||
start = closeIndex + 2;
|
||||
}
|
||||
|
||||
return new EvaluatedQuery(sb.ToString(), parameters);
|
||||
}
|
||||
|
||||
private object? ResolveValue(string key)
|
||||
{
|
||||
return key switch
|
||||
{
|
||||
"Workflow.Definition.Id" => executionContext.Workflow.Identity.DefinitionId,
|
||||
"Workflow.Definition.Version.Id" => executionContext.Workflow.Identity.Id,
|
||||
"Workflow.Definition.Version" => executionContext.Workflow.Identity.Version,
|
||||
"Workflow.Instance.Id" => activityContext.WorkflowExecutionContext.Id,
|
||||
"Correlation.Id" => activityContext.WorkflowExecutionContext.CorrelationId,
|
||||
"LastResult" => expressionContext.GetLastResult(),
|
||||
var i when i.StartsWith("Input.") => executionContext.Input.TryGetValue(i.Substring(6), out var v) ? v : null,
|
||||
var o when o.StartsWith("Output.") => executionContext.Output.TryGetValue(o.Substring(7), out var v) ? v : null,
|
||||
var v when v.StartsWith("Variable.") => expressionContext.GetVariableInScope(v.Substring(9)) ?? null,
|
||||
// OBSOLETE: This is deprecated and will be removed in a future version. Use 'Variable.' instead.
|
||||
var v when v.StartsWith("Variables.") => expressionContext.GetVariableInScope(v.Substring(10)) ?? null,
|
||||
_ => throw new NullReferenceException($"No matching property found for {{{{{key}}}}}.")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Reflection;
|
||||
using Elsa.Sql.Contracts;
|
||||
using Elsa.Workflows.UIHints.Dropdown;
|
||||
|
||||
namespace Elsa.Sql.UIHints;
|
||||
|
||||
/// <summary>
|
||||
/// Provides registered clients for the Client input field.
|
||||
/// </summary>
|
||||
/// <param name="sqlClientNamesProvider"></param>
|
||||
public class SqlClientsDropDownProvider(ISqlClientNamesProvider sqlClientNamesProvider) : DropDownOptionsProviderBase
|
||||
{
|
||||
protected override async ValueTask<ICollection<SelectListItem>> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken)
|
||||
{
|
||||
var clients = await sqlClientNamesProvider.GetRegisteredSqlClientNamesAsync(cancellationToken);
|
||||
return clients.Select(x => new SelectListItem(x.Key, x.Key)).ToList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user