diff --git a/InfluxData.Net.Common/Helpers/StringExtensions.cs b/InfluxData.Net.Common/Helpers/StringExtensions.cs new file mode 100644 index 0000000..5cd64d1 --- /dev/null +++ b/InfluxData.Net.Common/Helpers/StringExtensions.cs @@ -0,0 +1,32 @@ +using System; +using System.Text.RegularExpressions; + +namespace InfluxData.Net.Common +{ + public static class StringExtensions + { + // http://www.mvvm.ro/2011/03/sanitize-strings-against-sql-injection.html + public static string Sanitize(this string stringValue) + { + if (null == stringValue) + return stringValue; + return stringValue + .RegexReplace("-{2,}", "-") + .RegexReplace(@"[*/]+", string.Empty) + .RegexReplace(@"(;|\s)(exec|execute|select|insert|update|delete|create|alter|drop|rename|truncate|backup|restore)\s", + string.Empty, RegexOptions.IgnoreCase); + } + + + private static string RegexReplace(this string stringValue, string matchPattern, string toReplaceWith) + { + return Regex.Replace(stringValue, matchPattern, toReplaceWith); + } + + private static string RegexReplace(this string stringValue, string matchPattern, string toReplaceWith, RegexOptions regexOptions) + { + return Regex.Replace(stringValue, matchPattern, toReplaceWith, regexOptions); + } + + } +} diff --git a/InfluxData.Net.InfluxDb/ClientModules/BasicClientModule.cs b/InfluxData.Net.InfluxDb/ClientModules/BasicClientModule.cs index 1ce485e..e963ed0 100644 --- a/InfluxData.Net.InfluxDb/ClientModules/BasicClientModule.cs +++ b/InfluxData.Net.InfluxDb/ClientModules/BasicClientModule.cs @@ -7,6 +7,8 @@ using InfluxData.Net.InfluxDb.RequestClients; using InfluxData.Net.InfluxDb.ResponseParsers; using InfluxData.Net.Common.Constants; +using InfluxData.Net.InfluxDb.Helpers; +using System; namespace InfluxData.Net.InfluxDb.ClientModules { @@ -14,6 +16,13 @@ public class BasicClientModule : ClientModuleBase, IBasicClientModule { private readonly IBasicResponseParser _basicResponseParser; + public Task> QueryAsync(string query, object param = null, string dbName = null, string epochFormat = null, long? chunkSize = default(long?)) + { + var buildQuery = QueryHelpers.BuildParameterizedQuery(query, param); + + return this.QueryAsync(buildQuery, dbName, epochFormat, chunkSize); + } + public virtual async Task> QueryAsync(string query, string dbName = null, string epochFormat = null, long? chunkSize = null) { var series = await base.ResolveSingleGetSeriesResultAsync(query, dbName, epochFormat, chunkSize).ConfigureAwait(false); diff --git a/InfluxData.Net.InfluxDb/ClientModules/IBasicClientModule.cs b/InfluxData.Net.InfluxDb/ClientModules/IBasicClientModule.cs index 2c11b08..d5ba931 100644 --- a/InfluxData.Net.InfluxDb/ClientModules/IBasicClientModule.cs +++ b/InfluxData.Net.InfluxDb/ClientModules/IBasicClientModule.cs @@ -9,6 +9,18 @@ namespace InfluxData.Net.InfluxDb.ClientModules { public interface IBasicClientModule { + /// + /// Executes a parameterized query against the database. If chunkSize is specified, responses + /// will be broken down by number of returned rows. + /// + /// Query to execute. + /// The parameters to pass, if any. (OPTIONAL) + /// Database name. (OPTIONAL) + /// Epoch timestamp format. (OPTIONAL) + /// Maximum number of rows per chunk. (OPTIONAL) + /// + Task> QueryAsync(string query, object param = null, string dbName = null, string epochFormat = null, long? chunkSize = null); + /// /// Executes a query against the database. If chunkSize is specified, responses /// will be broken down by number of returned rows. diff --git a/InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs b/InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs new file mode 100644 index 0000000..f51ed79 --- /dev/null +++ b/InfluxData.Net.InfluxDb/Helpers/QueryHelpers.cs @@ -0,0 +1,52 @@ +using System; +using System.Reflection; +using System.Text.RegularExpressions; +using InfluxData.Net.Common; +using System.Linq; + +namespace InfluxData.Net.InfluxDb.Helpers +{ + public static class QueryHelpers + { + public static string BuildParameterizedQuery(string query, object param) + { + Type t = param.GetType(); + PropertyInfo[] pi = t.GetProperties(); + + + foreach (var propertyInfo in pi) + { + var regex = $@"@{propertyInfo.Name}(?!\w)"; + + if(!Regex.IsMatch(query, regex) && Nullable.GetUnderlyingType(propertyInfo.GetType()) != null) + throw new ArgumentException($"Missing parameter identifier for @{propertyInfo.Name}"); + + var paramValue = propertyInfo.GetValue(param); + if (paramValue == null) + continue; + + var paramType = paramValue.GetType(); + + if (!paramType.IsPrimitive && paramType != typeof(String) && paramType != typeof(DateTime)) + throw new NotSupportedException($"The type {paramType.Name} is not a supported query parameter type."); + + var sanitizedParamValue = paramValue; + + if (paramType == typeof(String)) + { + sanitizedParamValue = ((string)sanitizedParamValue).Sanitize(); + } + + while (Regex.IsMatch(query, regex)) + { + var match = Regex.Match(query, regex); + + query = query.Remove(match.Index, match.Length); + query = query.Insert(match.Index, $"{sanitizedParamValue}"); + } + } + + return query; + } + } +} diff --git a/InfluxData.Net.Tests/InfluxData.Net.Tests.csproj b/InfluxData.Net.Tests/InfluxData.Net.Tests.csproj index 09b2553..bb40465 100644 --- a/InfluxData.Net.Tests/InfluxData.Net.Tests.csproj +++ b/InfluxData.Net.Tests/InfluxData.Net.Tests.csproj @@ -32,4 +32,7 @@ PreserveNewest + + + \ No newline at end of file diff --git a/InfluxData.Net.Tests/InfluxDb/Helpers/QueryHelpersTests.cs b/InfluxData.Net.Tests/InfluxDb/Helpers/QueryHelpersTests.cs new file mode 100644 index 0000000..981243d --- /dev/null +++ b/InfluxData.Net.Tests/InfluxDb/Helpers/QueryHelpersTests.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using InfluxData.Net.InfluxDb.Helpers; +using Xunit; + +namespace InfluxData.Net.Tests +{ + [Trait("InfluxDb SerieExtensions", "Serie extensions")] + public class QueryHelpersTests + { + [Fact] + public void Building_Parameterized_Query_Returns_Correct_String() + { + var firstTag = "firstTag"; + var firstTagValue = "firstTagValue"; + + var firstField = "firstField"; + var firstFieldValue = "firstFieldValue"; + + var query = "SELECT * FROM fakeMeasurement " + + $"WHERE {firstTag} = @FirstTagValue " + + $"AND {firstField} = @FirstFieldValue"; + + var expectedNewQuery = "SELECT * FROM fakeMeasurement " + + $"WHERE {firstTag} = {firstTagValue} " + + $"AND {firstField} = {firstFieldValue}"; + + var actualNewQuery = QueryHelpers.BuildParameterizedQuery( + query, + new + { + @FirstTagValue = firstTagValue, + @FirstFieldValue = firstFieldValue + }); + + Assert.Equal(expectedNewQuery, actualNewQuery); + } + + + [Fact] + public void Using_Non_Primitive_And_Non_String_Type_In_Parameters_Throws_NotSupportedException() + { + var firstTag = "firstTag"; + var firstTagValue = "firstTagValue"; + + var firstField = "firstField"; + + var query = "SELECT * FROM fakeMeasurement " + + $"WHERE {firstTag} = @FirstTagValue " + + $"AND {firstField} = @FirstFieldValue"; + + Func func = new Func(() => + { + return QueryHelpers.BuildParameterizedQuery( + query, + new + { + @FirstTagValue = firstTagValue, + @FirstFieldValue = new List() { "NOT ACCEPTED" } + }); + }); + + Assert.Throws(typeof(NotSupportedException), func); + } + + [Fact] + public void Building_Parameterized_Query_With_Missing_Parameters_Throws_ArgumentException() + { + var firstTag = "firstTag"; + var firstTagValue = "firstTagValue"; + + var firstField = "firstField"; + + var query = "SELECT * FROM fakeMeasurement " + + $"WHERE {firstTag} = @FirstTagValue " + + $"AND {firstField} = @FirstFieldValue"; + + Func func = new Func(() => + { + return QueryHelpers.BuildParameterizedQuery( + query, + new + { + @FirstTagValue = firstTagValue + }); + }); + + Assert.Throws(typeof(ArgumentException), func); + } + } +} diff --git a/InfluxData.Net.Tests/Integration/InfluxDb/Tests/IntegrationBasic.cs b/InfluxData.Net.Tests/Integration/InfluxDb/Tests/IntegrationBasic.cs index dd2938f..a553968 100644 --- a/InfluxData.Net.Tests/Integration/InfluxDb/Tests/IntegrationBasic.cs +++ b/InfluxData.Net.Tests/Integration/InfluxDb/Tests/IntegrationBasic.cs @@ -225,6 +225,38 @@ public virtual async Task ClientQuery_OnExistingPoints_ShouldReturnSerieCollecti result.First().Values.Should().HaveCount(3); } + [Fact] + public virtual async Task ClientQuery_Parameterized_OnExistingPoints_ShouldReturnSerieCollection() + { + var points = await _fixture.MockAndWritePoints(3); + + var firstTag = points.First().Tags.First().Key; + var firstTagValue = points.First().Tags.First().Value; + + var firstField = points.First().Fields.First().Key; + var firstFieldValue = points.First().Fields.First().Value; + + var query = $"SELECT * FROM {points.First().Name} " + + $@"WHERE {firstTag} = '@FirstTagValueParam' " + + $@"AND {firstField} = @FirstFieldValueParam"; + + + var result = await _fixture.Sut.Client.QueryAsync( + query, + new + { + @FirstTagValueParam = firstTagValue, + @FirstFieldValueParam = firstFieldValue + }, _fixture.DbName); + + var t = result.First(); + + result.Should().NotBeNull(); + result.Should().HaveCount(1); + result.First().Name.Should().Be(points.First().Name); + result.First().Values.Should().HaveCount(1); + } + [Fact] public virtual async Task ClientQueryMultiple_OnExistingPoints_ShouldReturnSerieCollection() {