diff --git a/Keen.NET.Test/ApiResponses/GetDatasetDefinition.json b/Keen.NET.Test/ApiResponses/GetDatasetDefinition.json new file mode 100644 index 0000000..d225fdc --- /dev/null +++ b/Keen.NET.Test/ApiResponses/GetDatasetDefinition.json @@ -0,0 +1,23 @@ +{ + "dataset_name": "count-purchases-gte-100-by-country-daily", + "display_name": "Count Daily Product Purchases Over $100 by Country", + "query": { + "project_id": "5011efa95f546f2ce2000000", + "analysis_type": "count", + "event_collection": "purchases", + "filters": [ + { + "property_name": "price", + "operator": "gte", + "property_value": 100 + } + ], + "timeframe": "this_500_days", + "interval": "daily", + "group_by": [ "ip_geo_info.country" ] + }, + "index_by": [ "product.id" ], + "last_scheduled_date": "2016-11-04T18:52:36.323Z", + "latest_subtimeframe_available": "2016-11-05T00:00:00.000Z", + "milliseconds_behind": 3600000 +} \ No newline at end of file diff --git a/Keen.NET.Test/ApiResponses/GetDatasetResults.json b/Keen.NET.Test/ApiResponses/GetDatasetResults.json new file mode 100644 index 0000000..9220db7 --- /dev/null +++ b/Keen.NET.Test/ApiResponses/GetDatasetResults.json @@ -0,0 +1,52 @@ +{ + "result": [ + { + "timeframe": { + "start": "2016-11-02T00:00:00.000Z", + "end": "2016-11-03T00:00:00.000Z" + }, + "value": [ + { + "item.name": "Golden Widget", + "result": 0 + }, + { + "item.name": "Silver Widget", + "result": 18 + }, + { + "item.name": "Bronze Widget", + "result": 1 + }, + { + "item.name": "Platinum Widget", + "result": 9 + } + ] + }, + { + "timeframe": { + "start": "2016-11-03T00:00:00.000Z", + "end": "2016-11-04T00:00:00.000Z" + }, + "value": [ + { + "item.name": "Golden Widget", + "result": 1 + }, + { + "item.name": "Silver Widget", + "result": 13 + }, + { + "item.name": "Bronze Widget", + "result": 0 + }, + { + "item.name": "Platinum Widget", + "result": 3 + } + ] + } + ] +} \ No newline at end of file diff --git a/Keen.NET.Test/ApiResponses/ListDatasetDefinitions.json b/Keen.NET.Test/ApiResponses/ListDatasetDefinitions.json new file mode 100644 index 0000000..c7748f9 --- /dev/null +++ b/Keen.NET.Test/ApiResponses/ListDatasetDefinitions.json @@ -0,0 +1,60 @@ +{ + "datasets": [ + { + "project_id": "PROJECT_ID", + "organization_id": "ORGANIZATION_ID", + "dataset_name": "DATASET_NAME_1", + "display_name": "a first dataset wee", + "query": { + "project_id": "PROJECT_ID", + "analysis_type": "count", + "event_collection": "best collection", + "filters": [ + { + "property_name": "request.foo", + "operator": "lt", + "property_value": 300 + } + ], + "timeframe": "this_500_hours", + "timezone": "US/Pacific", + "interval": "hourly", + "group_by": [ + "exception.name" + ] + }, + "index_by": [ + "project.id" + ], + "last_scheduled_date": "2016-11-04T18:03:38.430Z", + "latest_subtimeframe_available": "2016-11-04T19:00:00.000Z", + "milliseconds_behind": 3600000 + }, + { + "project_id": "PROJECT_ID", + "organization_id": "ORGANIZATION_ID", + "dataset_name": "DATASET_NAME_10", + "display_name": "tenth dataset wee", + "query": { + "project_id": "PROJECT_ID", + "analysis_type": "count", + "event_collection": "tenth best collection", + "filters": [], + "timeframe": "this_500_days", + "timezone": "UTC", + "interval": "daily", + "group_by": [ + "analysis_type" + ] + }, + "index_by": [ + "project.organization.id" + ], + "last_scheduled_date": "2016-11-04T19:28:36.639Z", + "latest_subtimeframe_available": "2016-11-05T00:00:00.000Z", + "milliseconds_behind": 3600000 + } + ], + "next_page_url": "https://api.keen.io/3.0/projects/PROJECT_ID/datasets?limit=LIMIT&after_name=DATASET_NAME_10", + "count": 4 +} \ No newline at end of file diff --git a/Keen.NET.Test/DataSetTests_Integration.cs b/Keen.NET.Test/DataSetTests_Integration.cs new file mode 100644 index 0000000..4169380 --- /dev/null +++ b/Keen.NET.Test/DataSetTests_Integration.cs @@ -0,0 +1,435 @@ +using Keen.Core; +using Keen.Core.Dataset; +using Keen.Core.Query; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + + +namespace Keen.Net.Test +{ + [TestFixture] + public class DatasetTests_Integration : TestBase + { + private const string _datasetName = "video-view"; + private const string _indexBy = "12"; + private const string _timeframe = "this_12_days"; + + + [Test] + public void Results_Success() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/GetDatasetResults.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + var dataset = client.QueryDataset(_datasetName, _indexBy, _timeframe); + + Assert.IsNotNull(dataset); + Assert.IsNotNull(dataset["result"]); + } + + [Test] + public void Definition_Success() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/GetDatasetDefinition.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + var dataset = client.GetDatasetDefinition(_datasetName); + + AssertDatasetIsPopulated(dataset); + } + + [Test] + public void ListDefinitions_Success() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/ListDatasetDefinitions.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + var datasetCollection = client.ListDatasetDefinitions(); + + Assert.IsNotNull(datasetCollection); + Assert.IsNotNull(datasetCollection.Datasets); + Assert.IsTrue(datasetCollection.Datasets.Any()); + Assert.IsTrue(!string.IsNullOrWhiteSpace(datasetCollection.NextPageUrl)); + + foreach (var item in datasetCollection.Datasets) + { + AssertDatasetIsPopulated(item); + } + } + + [Test] + public void ListAllDefinitions_Success() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/ListDatasetDefinitions.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + var datasetCollection = client.ListAllDatasetDefinitions(); + + Assert.IsNotNull(datasetCollection); + Assert.IsTrue(datasetCollection.Any()); + + foreach (var item in datasetCollection) + { + AssertDatasetIsPopulated(item); + } + } + + [Test] + public void Delete_Success() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForDeleteAsync(string.Empty); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + client.DeleteDataset("datasetName"); + } + + [Test] + public void CreateDataset_Success() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/GetDatasetDefinition.json"); + + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForPutAsync(apiResponse); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + var newDataSet = CreateDatasetDefinition(); + var dataset = client.CreateDataset(newDataSet); + + AssertDatasetIsPopulated(dataset); + } + + [Test] + public void DatasetValidation_Throws() + { + var dataset = new DatasetDefinition(); + + Assert.Throws(() => dataset.Validate()); + + dataset.DatasetName = "count-purchases-gte-100-by-country-daily"; + + Assert.Throws(() => dataset.Validate()); + + dataset.DisplayName = "Count Daily Product Purchases Over $100 by Country"; + + Assert.Throws(() => dataset.Validate()); + + dataset.IndexBy = new List { "product.id" }; + + Assert.Throws(() => dataset.Validate()); + + dataset.Query = new QueryDefinition(); + + Assert.Throws(() => dataset.Validate()); + + dataset.Query.AnalysisType = "count"; + + Assert.Throws(() => dataset.Validate()); + + dataset.Query.EventCollection = "purchases"; + + Assert.Throws(() => dataset.Validate()); + + dataset.Query.Timeframe = "this_500_days"; + + Assert.Throws(() => dataset.Validate()); + + dataset.Query.Interval = "daily"; + + Assert.DoesNotThrow(() => dataset.Validate()); + } + + [Test] + public void Results_Throws() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync("{}", HttpStatusCode.InternalServerError); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + Assert.Throws(() => client.QueryDataset(null, _indexBy, _timeframe)); + Assert.Throws(() => client.QueryDataset(_datasetName, null, _timeframe)); + Assert.Throws(() => client.QueryDataset(_datasetName, _indexBy, null)); + + Assert.Throws(() => client.QueryDataset(_datasetName, _indexBy, _timeframe)); + + var brokenClient = new KeenClient(new ProjectSettingsProvider("5011efa95f546f2ce2000000", + null, + Environment.GetEnvironmentVariable("KEEN_WRITE_KEY") ?? "", + Environment.GetEnvironmentVariable("KEEN_READ_KEY") ?? "", + Environment.GetEnvironmentVariable("KEEN_SERVER_URL") ?? KeenConstants.ServerAddress + "/" + KeenConstants.ApiVersion + "/"), + httpClientProvider); + + Assert.Throws(() => brokenClient.QueryDataset(_datasetName, _indexBy, _timeframe)); + } + + [Test] + public void Definition_Throws() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync("{}", HttpStatusCode.InternalServerError); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + Assert.Throws(() => client.GetDatasetDefinition(null)); + Assert.Throws(() => client.GetDatasetDefinition(_datasetName)); + + var brokenClient = new KeenClient(new ProjectSettingsProvider("5011efa95f546f2ce2000000", + null, + Environment.GetEnvironmentVariable("KEEN_WRITE_KEY") ?? "", + Environment.GetEnvironmentVariable("KEEN_READ_KEY") ?? "", + Environment.GetEnvironmentVariable("KEEN_SERVER_URL") ?? KeenConstants.ServerAddress + "/" + KeenConstants.ApiVersion + "/"), + httpClientProvider); + + Assert.Throws(() => brokenClient.GetDatasetDefinition(_datasetName)); + } + + [Test] + public void ListDefinitions_Throws() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync("{}", HttpStatusCode.InternalServerError); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + Assert.Throws(() => client.ListDatasetDefinitions()); + + var brokenClient = new KeenClient(new ProjectSettingsProvider("5011efa95f546f2ce2000000", + null, + Environment.GetEnvironmentVariable("KEEN_WRITE_KEY") ?? "", + Environment.GetEnvironmentVariable("KEEN_READ_KEY") ?? "", + Environment.GetEnvironmentVariable("KEEN_SERVER_URL") ?? KeenConstants.ServerAddress + "/" + KeenConstants.ApiVersion + "/"), + httpClientProvider); + + Assert.Throws(() => brokenClient.ListDatasetDefinitions()); + } + + [Test] + public void DeleteDataset_Throws() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForDeleteAsync("{}", HttpStatusCode.InternalServerError); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + Assert.Throws(() => client.DeleteDataset(null)); + Assert.Throws(() => client.DeleteDataset(_datasetName)); + + var brokenClient = new KeenClient(new ProjectSettingsProvider("5011efa95f546f2ce2000000", + null, + Environment.GetEnvironmentVariable("KEEN_WRITE_KEY") ?? "", + Environment.GetEnvironmentVariable("KEEN_READ_KEY") ?? "", + Environment.GetEnvironmentVariable("KEEN_SERVER_URL") ?? KeenConstants.ServerAddress + "/" + KeenConstants.ApiVersion + "/"), + httpClientProvider); + + Assert.Throws(() => brokenClient.DeleteDataset(_datasetName)); + } + + [Test] + public void CreateDataset_Throws() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForDeleteAsync("{}", HttpStatusCode.InternalServerError); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + Assert.Throws(() => client.CreateDataset(null)); + + var brokenClient = new KeenClient(new ProjectSettingsProvider("5011efa95f546f2ce2000000", + null, + Environment.GetEnvironmentVariable("KEEN_WRITE_KEY") ?? "", + Environment.GetEnvironmentVariable("KEEN_READ_KEY") ?? "", + Environment.GetEnvironmentVariable("KEEN_SERVER_URL") ?? KeenConstants.ServerAddress + "/" + KeenConstants.ApiVersion + "/"), + httpClientProvider); + + Assert.Throws(() => brokenClient.CreateDataset(CreateDatasetDefinition())); + } + + private string GetApiResponsesPath() + { + var localPath = AppDomain.CurrentDomain.BaseDirectory; + var apiResponsesPath = $"{localPath}/ApiResponses"; + + return apiResponsesPath; + } + + private IKeenHttpClientProvider GetMockHttpClientProviderForGetAsync(string response, HttpStatusCode status = HttpStatusCode.OK) + { + var httpResponseMessage = new HttpResponseMessage + { + Content = new StringContent(response), + StatusCode = status + }; + + var mockHttpClient = new Mock(); + mockHttpClient.Setup(m => m.GetAsync( + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(httpResponseMessage)); + + return new TestKeenHttpClientProvider + { + ProvideKeenHttpClient = (url) => mockHttpClient.Object + }; + } + + private IKeenHttpClientProvider GetMockHttpClientProviderForPutAsync(string response) + { + var httpResponseMessage = new HttpResponseMessage + { + Content = new StringContent(response), + StatusCode = HttpStatusCode.Created + }; + + var mockHttpClient = new Mock(); + mockHttpClient.Setup(m => m.PutAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(httpResponseMessage)); + + return new TestKeenHttpClientProvider + { + ProvideKeenHttpClient = (url) => mockHttpClient.Object + }; + } + + private IKeenHttpClientProvider GetMockHttpClientProviderForDeleteAsync(string response, HttpStatusCode status = HttpStatusCode.NoContent) + { + var httpResponseMessage = new HttpResponseMessage + { + Content = new StringContent(response), + StatusCode = status + }; + + var mockHttpClient = new Mock(); + mockHttpClient.Setup(m => m.DeleteAsync( + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(httpResponseMessage)); + + return new TestKeenHttpClientProvider + { + ProvideKeenHttpClient = (url) => mockHttpClient.Object + }; + } + + private void AssertDatasetIsPopulated(DatasetDefinition dataset) + { + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.DatasetName)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.DisplayName)); + Assert.IsNotEmpty(dataset.IndexBy); + + if (UseMocks) + { + Assert.IsNotNull(dataset.LastScheduledDate); + Assert.IsNotNull(dataset.LatestSubtimeframeAvailable); + } + + Assert.IsNotNull(dataset.Query); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.Query.ProjectId)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.Query.AnalysisType)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.Query.EventCollection)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.Query.Timeframe)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.Query.Interval)); + + // TODO : We'll need to do some setup to actually get this to run automatically + // with !UseMocks...and take into account that it can "take up to an hour for a newly + // created dataset to compute results for the first time." + Assert.IsNotNull(dataset.Query.GroupBy); + Assert.IsTrue(dataset.Query.GroupBy.Count() == 1); + + if (dataset.Query.Filters != null) + { + foreach (var filter in dataset.Query.Filters) + { + AssertFilterIsPopulated(filter); + } + } + } + + private void AssertFilterIsPopulated(QueryFilter filter) + { + Assert.IsNotNull(filter); + Assert.IsTrue(!string.IsNullOrWhiteSpace(filter.PropertyName)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(filter.Operator)); + } + + private DatasetDefinition CreateDatasetDefinition() + { + return new DatasetDefinition + { + DatasetName = "count-purchases-gte-100-by-country-daily", + DisplayName = "Count Daily Product Purchases Over $100 by Country", + IndexBy = new List { "product.id" }, + Query = new QueryDefinition + { + AnalysisType = "count", + EventCollection = "purchases", + Timeframe = "this_500_days", + Interval = "daily" + } + }; + } + } +} diff --git a/Keen.NET.Test/DatasetTests.cs b/Keen.NET.Test/DatasetTests.cs new file mode 100644 index 0000000..ae9bce5 --- /dev/null +++ b/Keen.NET.Test/DatasetTests.cs @@ -0,0 +1,161 @@ +using Keen.Core; +using Keen.Core.Dataset; +using Keen.Core.Query; +using Moq; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Threading.Tasks; + + +namespace Keen.Net.Test +{ + [TestFixture] + public class DatasetTests : TestBase + { + private const string _datasetName = "video-view"; + private const string _indexBy = "12"; + private const int _listDatasetLimit = 1; + + + [Test] + public void GetDatasetResults_Success() + { + var timeframe = QueryRelativeTimeframe.ThisNMinutes(12); + var result = new JObject(); + var client = new KeenClient(SettingsEnv); + + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.GetResultsAsync( + It.Is(n => n == _datasetName), + It.Is(i => i == _indexBy), + It.Is(t => t == timeframe.ToString()))) + .ReturnsAsync(result); + + client.Datasets = datasetMock.Object; + } + + var dataset = client.QueryDataset(_datasetName, _indexBy, timeframe.ToString()); + Assert.IsNotNull(dataset); + + datasetMock?.VerifyAll(); + } + + [Test] + public void GetDatasetDefinition_Success() + { + var result = new DatasetDefinition(); + var client = new KeenClient(SettingsEnv); + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.GetDefinitionAsync( + It.Is(n => n == _datasetName))) + .ReturnsAsync(result); + + client.Datasets = datasetMock.Object; + } + + var datasetDefinition = client.GetDatasetDefinition(_datasetName); + Assert.IsNotNull(datasetDefinition); + + datasetMock?.VerifyAll(); + } + + [Test] + public void ListDatasetDefinitions_Success() + { + var result = new DatasetDefinitionCollection(); + var client = new KeenClient(SettingsEnv); + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.ListDefinitionsAsync( + It.Is(n => n == _listDatasetLimit), + It.Is(n => n == _datasetName))) + .ReturnsAsync(result); + + client.Datasets = datasetMock.Object; + } + + var datasetDefinitionCollection = client.ListDatasetDefinitions(_listDatasetLimit, _datasetName); + Assert.IsNotNull(datasetDefinitionCollection); + + datasetMock?.VerifyAll(); + } + + [Test] + public void ListDatasetAllDefinitions_Success() + { + IEnumerable result = new List(); + var client = new KeenClient(SettingsEnv); + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.ListAllDefinitionsAsync()) + .ReturnsAsync(result); + + client.Datasets = datasetMock.Object; + } + + var datasetDefinitions = client.ListAllDatasetDefinitions(); + Assert.IsNotNull(datasetDefinitions); + + datasetMock?.VerifyAll(); + } + + [Test] + public void CreateDataset_Success() + { + var result = new DatasetDefinition(); + var client = new KeenClient(SettingsEnv); + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.CreateDatasetAsync( + It.Is(n => n != null))) + .ReturnsAsync(result); + + client.Datasets = datasetMock.Object; + } + + var datasetDefinition = client.CreateDataset(new DatasetDefinition()); + Assert.IsNotNull(datasetDefinition); + + datasetMock?.VerifyAll(); + } + + [Test] + public void DeleteDataset_Success() + { + var client = new KeenClient(SettingsEnv); + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.DeleteDatasetAsync( + It.Is(n => n == _datasetName))) + .Returns(Task.Delay(0)); + + client.Datasets = datasetMock.Object; + } + + client.DeleteDataset(_datasetName); + + datasetMock?.VerifyAll(); + } + } +} diff --git a/Keen.NET.Test/EventCacheTest.cs b/Keen.NET.Test/EventCacheTest.cs index 9a39d13..135a71a 100644 --- a/Keen.NET.Test/EventCacheTest.cs +++ b/Keen.NET.Test/EventCacheTest.cs @@ -1,10 +1,11 @@ using Keen.Core; using Keen.Core.EventCache; +using Newtonsoft.Json.Linq; using NUnit.Framework; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Newtonsoft.Json.Linq; + namespace Keen.Net.Test { diff --git a/Keen.NET.Test/Keen.NET.Test.csproj b/Keen.NET.Test/Keen.NET.Test.csproj index c0e0162..3f05425 100644 --- a/Keen.NET.Test/Keen.NET.Test.csproj +++ b/Keen.NET.Test/Keen.NET.Test.csproj @@ -100,6 +100,8 @@ Properties\SharedVersionInfo.cs + + @@ -120,6 +122,15 @@ + + Always + + + Always + + + Always + diff --git a/Keen.NET.Test/KeenClientTest.cs b/Keen.NET.Test/KeenClientTest.cs index d00b2fa..794d057 100644 --- a/Keen.NET.Test/KeenClientTest.cs +++ b/Keen.NET.Test/KeenClientTest.cs @@ -1,4 +1,4 @@ -using Keen.Core; +using Keen.Core; using Keen.Core.EventCache; using Moq; using Newtonsoft.Json.Linq; diff --git a/Keen.Net/EventCacheMemory.cs b/Keen.Net/EventCacheMemory.cs index 38612a0..3e69ed2 100644 --- a/Keen.Net/EventCacheMemory.cs +++ b/Keen.Net/EventCacheMemory.cs @@ -1,11 +1,10 @@ using Keen.Core; using Keen.Core.EventCache; -using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; + namespace Keen.Net { /// diff --git a/Keen.NetStandard.Test/ApiResponses/GetDatasetDefinition.json b/Keen.NetStandard.Test/ApiResponses/GetDatasetDefinition.json new file mode 100644 index 0000000..d225fdc --- /dev/null +++ b/Keen.NetStandard.Test/ApiResponses/GetDatasetDefinition.json @@ -0,0 +1,23 @@ +{ + "dataset_name": "count-purchases-gte-100-by-country-daily", + "display_name": "Count Daily Product Purchases Over $100 by Country", + "query": { + "project_id": "5011efa95f546f2ce2000000", + "analysis_type": "count", + "event_collection": "purchases", + "filters": [ + { + "property_name": "price", + "operator": "gte", + "property_value": 100 + } + ], + "timeframe": "this_500_days", + "interval": "daily", + "group_by": [ "ip_geo_info.country" ] + }, + "index_by": [ "product.id" ], + "last_scheduled_date": "2016-11-04T18:52:36.323Z", + "latest_subtimeframe_available": "2016-11-05T00:00:00.000Z", + "milliseconds_behind": 3600000 +} \ No newline at end of file diff --git a/Keen.NetStandard.Test/ApiResponses/GetDatasetResults.json b/Keen.NetStandard.Test/ApiResponses/GetDatasetResults.json new file mode 100644 index 0000000..9220db7 --- /dev/null +++ b/Keen.NetStandard.Test/ApiResponses/GetDatasetResults.json @@ -0,0 +1,52 @@ +{ + "result": [ + { + "timeframe": { + "start": "2016-11-02T00:00:00.000Z", + "end": "2016-11-03T00:00:00.000Z" + }, + "value": [ + { + "item.name": "Golden Widget", + "result": 0 + }, + { + "item.name": "Silver Widget", + "result": 18 + }, + { + "item.name": "Bronze Widget", + "result": 1 + }, + { + "item.name": "Platinum Widget", + "result": 9 + } + ] + }, + { + "timeframe": { + "start": "2016-11-03T00:00:00.000Z", + "end": "2016-11-04T00:00:00.000Z" + }, + "value": [ + { + "item.name": "Golden Widget", + "result": 1 + }, + { + "item.name": "Silver Widget", + "result": 13 + }, + { + "item.name": "Bronze Widget", + "result": 0 + }, + { + "item.name": "Platinum Widget", + "result": 3 + } + ] + } + ] +} \ No newline at end of file diff --git a/Keen.NetStandard.Test/ApiResponses/ListDatasetDefinitions.json b/Keen.NetStandard.Test/ApiResponses/ListDatasetDefinitions.json new file mode 100644 index 0000000..c7748f9 --- /dev/null +++ b/Keen.NetStandard.Test/ApiResponses/ListDatasetDefinitions.json @@ -0,0 +1,60 @@ +{ + "datasets": [ + { + "project_id": "PROJECT_ID", + "organization_id": "ORGANIZATION_ID", + "dataset_name": "DATASET_NAME_1", + "display_name": "a first dataset wee", + "query": { + "project_id": "PROJECT_ID", + "analysis_type": "count", + "event_collection": "best collection", + "filters": [ + { + "property_name": "request.foo", + "operator": "lt", + "property_value": 300 + } + ], + "timeframe": "this_500_hours", + "timezone": "US/Pacific", + "interval": "hourly", + "group_by": [ + "exception.name" + ] + }, + "index_by": [ + "project.id" + ], + "last_scheduled_date": "2016-11-04T18:03:38.430Z", + "latest_subtimeframe_available": "2016-11-04T19:00:00.000Z", + "milliseconds_behind": 3600000 + }, + { + "project_id": "PROJECT_ID", + "organization_id": "ORGANIZATION_ID", + "dataset_name": "DATASET_NAME_10", + "display_name": "tenth dataset wee", + "query": { + "project_id": "PROJECT_ID", + "analysis_type": "count", + "event_collection": "tenth best collection", + "filters": [], + "timeframe": "this_500_days", + "timezone": "UTC", + "interval": "daily", + "group_by": [ + "analysis_type" + ] + }, + "index_by": [ + "project.organization.id" + ], + "last_scheduled_date": "2016-11-04T19:28:36.639Z", + "latest_subtimeframe_available": "2016-11-05T00:00:00.000Z", + "milliseconds_behind": 3600000 + } + ], + "next_page_url": "https://api.keen.io/3.0/projects/PROJECT_ID/datasets?limit=LIMIT&after_name=DATASET_NAME_10", + "count": 4 +} \ No newline at end of file diff --git a/Keen.NetStandard.Test/DatasetTests.cs b/Keen.NetStandard.Test/DatasetTests.cs new file mode 100644 index 0000000..e9ab361 --- /dev/null +++ b/Keen.NetStandard.Test/DatasetTests.cs @@ -0,0 +1,160 @@ +using Keen.Core.Dataset; +using Keen.Core.Query; +using Moq; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Threading.Tasks; + + +namespace Keen.Core.Test +{ + [TestFixture] + public class DatasetTests : TestBase + { + private const string _datasetName = "video-view"; + private const string _indexBy = "12"; + private const int _listDatasetLimit = 1; + + + [Test] + public void GetDatasetResults_Success() + { + var timeframe = QueryRelativeTimeframe.ThisNMinutes(12); + var result = new JObject(); + var client = new KeenClient(SettingsEnv); + + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.GetResultsAsync( + It.Is(n => n == _datasetName), + It.Is(i => i == _indexBy), + It.Is(t => t == timeframe.ToString()))) + .ReturnsAsync(result); + + client.Datasets = datasetMock.Object; + } + + var dataset = client.QueryDataset(_datasetName, _indexBy, timeframe.ToString()); + Assert.IsNotNull(dataset); + + datasetMock?.VerifyAll(); + } + + [Test] + public void GetDatasetDefinition_Success() + { + var result = new DatasetDefinition(); + var client = new KeenClient(SettingsEnv); + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.GetDefinitionAsync( + It.Is(n => n == _datasetName))) + .ReturnsAsync(result); + + client.Datasets = datasetMock.Object; + } + + var datasetDefinition = client.GetDatasetDefinition(_datasetName); + Assert.IsNotNull(datasetDefinition); + + datasetMock?.VerifyAll(); + } + + [Test] + public void ListDatasetDefinitions_Success() + { + var result = new DatasetDefinitionCollection(); + var client = new KeenClient(SettingsEnv); + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.ListDefinitionsAsync( + It.Is(n => n == _listDatasetLimit), + It.Is(n => n == _datasetName))) + .ReturnsAsync(result); + + client.Datasets = datasetMock.Object; + } + + var datasetDefinitionCollection = client.ListDatasetDefinitions(_listDatasetLimit, _datasetName); + Assert.IsNotNull(datasetDefinitionCollection); + + datasetMock?.VerifyAll(); + } + + [Test] + public void ListDatasetAllDefinitions_Success() + { + IEnumerable result = new List(); + var client = new KeenClient(SettingsEnv); + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.ListAllDefinitionsAsync()) + .ReturnsAsync(result); + + client.Datasets = datasetMock.Object; + } + + var datasetDefinitions = client.ListAllDatasetDefinitions(); + Assert.IsNotNull(datasetDefinitions); + + datasetMock?.VerifyAll(); + } + + [Test] + public void CreateDataset_Success() + { + var result = new DatasetDefinition(); + var client = new KeenClient(SettingsEnv); + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.CreateDatasetAsync( + It.Is(n => n != null))) + .ReturnsAsync(result); + + client.Datasets = datasetMock.Object; + } + + var datasetDefinition = client.CreateDataset(new DatasetDefinition()); + Assert.IsNotNull(datasetDefinition); + + datasetMock?.VerifyAll(); + } + + [Test] + public void DeleteDataset_Success() + { + var client = new KeenClient(SettingsEnv); + Mock datasetMock = null; + + if (UseMocks) + { + datasetMock = new Mock(); + datasetMock.Setup(m => m.DeleteDatasetAsync( + It.Is(n => n == _datasetName))) + .Returns(Task.Delay(0)); + + client.Datasets = datasetMock.Object; + } + + client.DeleteDataset(_datasetName); + + datasetMock?.VerifyAll(); + } + } +} diff --git a/Keen.NetStandard.Test/DatasetTests_Integration.cs b/Keen.NetStandard.Test/DatasetTests_Integration.cs new file mode 100644 index 0000000..1af77c2 --- /dev/null +++ b/Keen.NetStandard.Test/DatasetTests_Integration.cs @@ -0,0 +1,498 @@ +using Keen.Core.Dataset; +using Keen.Core.Query; +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + + +namespace Keen.Core.Test +{ + [TestFixture] + public class DatasetTests_Integration : TestBase + { + private const string _datasetName = "video-view"; + private const string _indexBy = "12"; + private const string _timeframe = "this_12_days"; + + + [Test] + public void Results_Success() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/GetDatasetResults.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + var dataset = client.QueryDataset(_datasetName, _indexBy, _timeframe); + + Assert.IsNotNull(dataset); + Assert.IsNotNull(dataset["result"]); + } + + [Test] + public void Definition_Success() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/GetDatasetDefinition.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + var dataset = client.GetDatasetDefinition(_datasetName); + + AssertDatasetIsPopulated(dataset); + } + + [Test] + public void ListDefinitions_Success() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/ListDatasetDefinitions.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + var datasetCollection = client.ListDatasetDefinitions(); + + Assert.IsNotNull(datasetCollection); + Assert.IsNotNull(datasetCollection.Datasets); + Assert.IsTrue(datasetCollection.Datasets.Any()); + Assert.IsTrue(!string.IsNullOrWhiteSpace(datasetCollection.NextPageUrl)); + + foreach (var item in datasetCollection.Datasets) + { + AssertDatasetIsPopulated(item); + } + } + + [Test] + public void ListAllDefinitions_Success() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/ListDatasetDefinitions.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + var datasetCollection = client.ListAllDatasetDefinitions(); + + Assert.IsNotNull(datasetCollection); + Assert.IsTrue(datasetCollection.Any()); + + foreach (var item in datasetCollection) + { + AssertDatasetIsPopulated(item); + } + } + + [Test] + public void Delete_Success() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForDeleteAsync(string.Empty); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + client.DeleteDataset("datasetName"); + } + + [Test] + public void CreateDataset_Success() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/GetDatasetDefinition.json"); + + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForPutAsync(apiResponse); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + var newDataSet = CreateDatasetDefinition(); + var dataset = client.CreateDataset(newDataSet); + + AssertDatasetIsPopulated(dataset); + } + + [Test] + public void DatasetValidation_Throws() + { + var dataset = new DatasetDefinition(); + + Assert.Throws(() => dataset.Validate()); + + dataset.DatasetName = "count-purchases-gte-100-by-country-daily"; + + Assert.Throws(() => dataset.Validate()); + + dataset.DisplayName = "Count Daily Product Purchases Over $100 by Country"; + + Assert.Throws(() => dataset.Validate()); + + dataset.IndexBy = new List { "product.id" }; + + Assert.Throws(() => dataset.Validate()); + + dataset.Query = new QueryDefinition(); + + Assert.Throws(() => dataset.Validate()); + + dataset.Query.AnalysisType = "count"; + + Assert.Throws(() => dataset.Validate()); + + dataset.Query.EventCollection = "purchases"; + + Assert.Throws(() => dataset.Validate()); + + dataset.Query.Timeframe = "this_500_days"; + + Assert.Throws(() => dataset.Validate()); + + dataset.Query.Interval = "daily"; + + Assert.DoesNotThrow(() => dataset.Validate()); + } + + [Test] + public void Results_Throws_WrongKey() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/GetDatasetResults.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var brokenClient = new KeenClient(new ProjectSettingsProvider("5011efa95f546f2ce2000000", + Environment.GetEnvironmentVariable(KeenConstants.KeenMasterKey) ?? "", + Environment.GetEnvironmentVariable(KeenConstants.KeenWriteKey) ?? "", + null, + GetServerUrl()), + httpClientProvider); + + Assert.Throws(() => brokenClient.QueryDataset(_datasetName, _indexBy, _timeframe)); + } + + [Test] + public void Results_Throws_ServerError() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync("{}", HttpStatusCode.InternalServerError); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + Assert.Throws(() => client.QueryDataset(null, _indexBy, _timeframe)); + Assert.Throws(() => client.QueryDataset(_datasetName, null, _timeframe)); + Assert.Throws(() => client.QueryDataset(_datasetName, _indexBy, null)); + Assert.Throws(() => client.QueryDataset(_datasetName, _indexBy, _timeframe)); + } + + [Test] + public void Definition_Throws_WrongKey() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/GetDatasetDefinition.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var brokenClient = new KeenClient(new ProjectSettingsProvider("5011efa95f546f2ce2000000", + Environment.GetEnvironmentVariable(KeenConstants.KeenMasterKey) ?? "", + Environment.GetEnvironmentVariable(KeenConstants.KeenWriteKey) ?? "", + null, + GetServerUrl()), + httpClientProvider); + + Assert.Throws(() => brokenClient.GetDatasetDefinition(_datasetName)); + } + + [Test] + public void Definition_Throws_ServerError() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync("{}", HttpStatusCode.InternalServerError); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + Assert.Throws(() => client.GetDatasetDefinition(null)); + Assert.Throws(() => client.GetDatasetDefinition(_datasetName)); + } + + [Test] + public void ListDefinitions_Throws_WrongKey() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/ListDatasetDefinitions.json"); + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync(apiResponse); + } + + var brokenClient = new KeenClient(new ProjectSettingsProvider("5011efa95f546f2ce2000000", + Environment.GetEnvironmentVariable(KeenConstants.KeenMasterKey) ?? "", + Environment.GetEnvironmentVariable(KeenConstants.KeenWriteKey) ?? "", + null, + GetServerUrl()), + httpClientProvider); + + Assert.Throws(() => brokenClient.ListDatasetDefinitions()); + } + + [Test] + public void ListDefinitions_Throws_ServerError() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForGetAsync("{}", HttpStatusCode.InternalServerError); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + Assert.Throws(() => client.ListDatasetDefinitions()); + } + + [Test] + public void DeleteDataset_Throws_WrongKey() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForDeleteAsync(string.Empty); + } + + var brokenClient = new KeenClient(new ProjectSettingsProvider("5011efa95f546f2ce2000000", + null, + Environment.GetEnvironmentVariable(KeenConstants.KeenWriteKey) ?? "", + Environment.GetEnvironmentVariable(KeenConstants.KeenReadKey) ?? "", + GetServerUrl()), + httpClientProvider); + + Assert.Throws(() => brokenClient.DeleteDataset(_datasetName)); + } + + [Test] + public void DeleteDataset_Throws_ServerError() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForDeleteAsync("{}", HttpStatusCode.InternalServerError); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + Assert.Throws(() => client.DeleteDataset(null)); + Assert.Throws(() => client.DeleteDataset(_datasetName)); + } + + [Test] + public void CreateDataset_Throws_WrongKey() + { + var apiResponse = File.ReadAllText($"{GetApiResponsesPath()}/GetDatasetDefinition.json"); + + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForPutAsync(apiResponse); + } + + var brokenClient = new KeenClient(new ProjectSettingsProvider("5011efa95f546f2ce2000000", + null, + Environment.GetEnvironmentVariable(KeenConstants.KeenWriteKey) ?? "", + Environment.GetEnvironmentVariable(KeenConstants.KeenReadKey) ?? "", + GetServerUrl()), + httpClientProvider); + + Assert.Throws(() => brokenClient.CreateDataset(CreateDatasetDefinition())); + } + + [Test] + public void CreateDataset_Throws_ServerError() + { + IKeenHttpClientProvider httpClientProvider = null; + + if (UseMocks) + { + httpClientProvider = GetMockHttpClientProviderForDeleteAsync("{}", HttpStatusCode.InternalServerError); + } + + var client = new KeenClient(SettingsEnv, httpClientProvider); + + Assert.Throws(() => client.CreateDataset(null)); + } + + private string GetApiResponsesPath() + { + var localPath = AppDomain.CurrentDomain.BaseDirectory; + var apiResponsesPath = $"{localPath}/ApiResponses"; + + return apiResponsesPath; + } + + private IKeenHttpClientProvider GetMockHttpClientProviderForGetAsync(string response, HttpStatusCode status = HttpStatusCode.OK) + { + var httpResponseMessage = new HttpResponseMessage + { + Content = new StringContent(response), + StatusCode = status + }; + + var mockHttpClient = new Mock(); + mockHttpClient.Setup(m => m.GetAsync( + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(httpResponseMessage)); + + return new TestKeenHttpClientProvider + { + ProvideKeenHttpClient = (url) => mockHttpClient.Object + }; + } + + private IKeenHttpClientProvider GetMockHttpClientProviderForPutAsync(string response) + { + var httpResponseMessage = new HttpResponseMessage + { + Content = new StringContent(response), + StatusCode = HttpStatusCode.Created + }; + + var mockHttpClient = new Mock(); + mockHttpClient.Setup(m => m.PutAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(httpResponseMessage)); + + return new TestKeenHttpClientProvider + { + ProvideKeenHttpClient = (url) => mockHttpClient.Object + }; + } + + private IKeenHttpClientProvider GetMockHttpClientProviderForDeleteAsync(string response, HttpStatusCode status = HttpStatusCode.NoContent) + { + var httpResponseMessage = new HttpResponseMessage + { + Content = new StringContent(response), + StatusCode = status + }; + + var mockHttpClient = new Mock(); + mockHttpClient.Setup(m => m.DeleteAsync( + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(httpResponseMessage)); + + return new TestKeenHttpClientProvider + { + ProvideKeenHttpClient = (url) => mockHttpClient.Object + }; + } + + private void AssertDatasetIsPopulated(DatasetDefinition dataset) + { + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.DatasetName)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.DisplayName)); + Assert.IsNotEmpty(dataset.IndexBy); + + if (UseMocks) + { + Assert.IsNotNull(dataset.LastScheduledDate); + Assert.IsNotNull(dataset.LatestSubtimeframeAvailable); + } + + Assert.IsNotNull(dataset.Query); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.Query.ProjectId)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.Query.AnalysisType)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.Query.EventCollection)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.Query.Timeframe)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(dataset.Query.Interval)); + + // TODO : We'll need to do some setup to actually get this to run automatically + // with !UseMocks...and take into account that it can "take up to an hour for a newly + // created dataset to compute results for the first time." + Assert.IsNotNull(dataset.Query.GroupBy); + Assert.IsTrue(dataset.Query.GroupBy.Count() == 1); + + if (dataset.Query.Filters != null) + { + foreach (var filter in dataset.Query.Filters) + { + AssertFilterIsPopulated(filter); + } + } + } + + private void AssertFilterIsPopulated(QueryFilter filter) + { + Assert.IsNotNull(filter); + Assert.IsTrue(!string.IsNullOrWhiteSpace(filter.PropertyName)); + Assert.IsTrue(!string.IsNullOrWhiteSpace(filter.Operator)); + } + + private DatasetDefinition CreateDatasetDefinition() + { + return new DatasetDefinition + { + DatasetName = "count-purchases-gte-100-by-country-daily", + DisplayName = "Count Daily Product Purchases Over $100 by Country", + IndexBy = new List { "product.id" }, + Query = new QueryDefinition + { + AnalysisType = "count", + EventCollection = "purchases", + Timeframe = "this_500_days", + Interval = "daily" + } + }; + } + + private string GetServerUrl() + { + return Environment.GetEnvironmentVariable(KeenConstants.KeenServerUrl) ?? KeenConstants.ServerAddress + "/" + KeenConstants.ApiVersion + "/"; + } + } +} diff --git a/Keen.NetStandard.Test/EventCachePortableTestable.cs b/Keen.NetStandard.Test/EventCachePortableTestable.cs index a1b1b2e..d705c50 100644 --- a/Keen.NetStandard.Test/EventCachePortableTestable.cs +++ b/Keen.NetStandard.Test/EventCachePortableTestable.cs @@ -1,5 +1,5 @@ -using System.IO; -using System.Threading.Tasks; +using System.Threading.Tasks; + namespace Keen.Core.Test { diff --git a/Keen.NetStandard.Test/EventCacheTest.cs b/Keen.NetStandard.Test/EventCacheTest.cs index 21ddd9f..8394a8a 100644 --- a/Keen.NetStandard.Test/EventCacheTest.cs +++ b/Keen.NetStandard.Test/EventCacheTest.cs @@ -2,9 +2,10 @@ using Newtonsoft.Json.Linq; using NUnit.Framework; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; -using System.IO; + namespace Keen.Core.Test { diff --git a/Keen.NetStandard.Test/Keen.NetStandard.Test.csproj b/Keen.NetStandard.Test/Keen.NetStandard.Test.csproj index fcb4d8c..d94601c 100644 --- a/Keen.NetStandard.Test/Keen.NetStandard.Test.csproj +++ b/Keen.NetStandard.Test/Keen.NetStandard.Test.csproj @@ -24,4 +24,16 @@ + + + Always + + + Always + + + Always + + + diff --git a/Keen.NetStandard.Test/QueryTest.cs b/Keen.NetStandard.Test/QueryTest.cs index dfdb6ec..adc4f7d 100644 --- a/Keen.NetStandard.Test/QueryTest.cs +++ b/Keen.NetStandard.Test/QueryTest.cs @@ -74,7 +74,7 @@ public async Task AvailableQueries_Success() client.Queries = queryMock.Object; } - var response = await client.GetQueries(); + var response = await client.GetQueriesAsync(); Assert.True(response.Any(p => p.Key == "minimum")); Assert.True(response.Any(p => p.Key == "average")); Assert.True(response.Any(p => p.Key == "maximum")); diff --git a/Keen.NetStandard.Test/QueryTests_Integration.cs b/Keen.NetStandard.Test/QueryTests_Integration.cs index 7d0214d..2578791 100644 --- a/Keen.NetStandard.Test/QueryTests_Integration.cs +++ b/Keen.NetStandard.Test/QueryTests_Integration.cs @@ -161,7 +161,7 @@ public async Task Query_AvailableQueries_Success() new DelegatingHandlerMock(handler)) }); - var actualQueries = await client.GetQueries(); + var actualQueries = await client.GetQueriesAsync(); Assert.That(actualQueries, Is.EquivalentTo(expectedQueries)); } diff --git a/Keen.NetStandard/Dataset/DatasetDefinition.cs b/Keen.NetStandard/Dataset/DatasetDefinition.cs new file mode 100644 index 0000000..4439346 --- /dev/null +++ b/Keen.NetStandard/Dataset/DatasetDefinition.cs @@ -0,0 +1,76 @@ +using Keen.Core.Query; +using System; +using System.Collections.Generic; +using System.Linq; + + +namespace Keen.Core.Dataset +{ + public class DatasetDefinition + { + /// + /// Name of the dataset, which is used as an identifier. Must be unique per project. + /// + public string DatasetName { get; set; } + + /// + /// The human-readable string name for your Cached Dataset. + /// + public string DisplayName { get; set; } + + /// + /// Holds information describing the query which is cached by this Cached Dataset. + /// + public QueryDefinition Query { get; set; } + + /// + /// When the most recent computation was queued. + /// + public DateTime? LastScheduledDate { get; set; } + + /// + /// The most recent interval that has been computed for the Cached Dataset. + /// + public DateTime? LatestSubtimeframeAvailable { get; set; } + + /// + /// The difference between now and the most recent datapoint computed. + /// + public long MillisecondsBehind { get; set; } + + /// + /// The event property name of string values results are retrieved by. + /// + public IEnumerable IndexBy { get; set; } + } + + internal static class DatasetDefinitionExtensions + { + public static void Validate(this DatasetDefinition dataset) + { + if (string.IsNullOrWhiteSpace(dataset.DatasetName)) + { + throw new KeenException("DatasetDefinition must have a name."); + } + + if (string.IsNullOrWhiteSpace(dataset.DisplayName)) + { + throw new KeenException("DatasetDefinition must have a display name."); + } + + if (null == dataset.IndexBy || + string.IsNullOrWhiteSpace(dataset.IndexBy.FirstOrDefault())) + { + throw new KeenException("DatasetDefinition must specify a property by which to " + + "index."); + } + + if (null == dataset.Query) + { + throw new KeenException("DatasetDefinition must contain a query to be cached."); + } + + dataset.Query.ValidateForCachedDataset(); + } + } +} diff --git a/Keen.NetStandard/Dataset/DatasetDefinitionCollection.cs b/Keen.NetStandard/Dataset/DatasetDefinitionCollection.cs new file mode 100644 index 0000000..adf6a10 --- /dev/null +++ b/Keen.NetStandard/Dataset/DatasetDefinitionCollection.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + + +namespace Keen.Core.Dataset +{ + /// + /// A model for a collection of DatasetDefinitions with paging information. + /// + public class DatasetDefinitionCollection + { + /// + /// A list of the DatasetDefinitions returned in this page. + /// + public IEnumerable Datasets { get; set; } + + /// + /// The url of the next page of Dataset definitions. + /// + public string NextPageUrl { get; set; } + + /// + /// The total count of Cached Datasets for this project. + /// + public int Count { get; set; } + } +} diff --git a/Keen.NetStandard/Dataset/Datasets.cs b/Keen.NetStandard/Dataset/Datasets.cs new file mode 100644 index 0000000..b6759e0 --- /dev/null +++ b/Keen.NetStandard/Dataset/Datasets.cs @@ -0,0 +1,313 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + + +namespace Keen.Core.Dataset +{ + /// + /// Datasets implements the IDataset interface which represents the Keen.IO Cached Datasets + /// API methods. + /// + internal class Datasets : IDataset + { + private const int MAX_DATASET_DEFINITION_LIST_LIMIT = 100; + private static readonly JsonSerializerSettings SERIALIZER_SETTINGS = + new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }, + DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, + Formatting = Formatting.None + }; + + private readonly IKeenHttpClient _keenHttpClient; + private readonly string _cachedDatasetRelativeUrl; + private readonly string _masterKey; + private readonly string _readKey; + + + internal Datasets(IProjectSettings prjSettings, + IKeenHttpClientProvider keenHttpClientProvider) + { + if (null == prjSettings) + { + throw new ArgumentNullException(nameof(prjSettings), + "Project Settings must be provided."); + } + + if (null == keenHttpClientProvider) + { + throw new ArgumentNullException(nameof(keenHttpClientProvider), + "A KeenHttpClient provider must be provided."); + } + + if (string.IsNullOrWhiteSpace(prjSettings.KeenUrl) || + !Uri.IsWellFormedUriString(prjSettings.KeenUrl, UriKind.Absolute)) + { + throw new KeenException( + "A properly formatted KeenUrl must be provided via Project Settings."); + } + + var serverBaseUrl = new Uri(prjSettings.KeenUrl); + _keenHttpClient = keenHttpClientProvider.GetForUrl(serverBaseUrl); + _cachedDatasetRelativeUrl = + KeenHttpClient.GetRelativeUrl(prjSettings.ProjectId, + KeenConstants.DatasetsResource); + + _masterKey = prjSettings.MasterKey; + _readKey = prjSettings.ReadKey; + } + + public async Task GetResultsAsync(string datasetName, + string indexBy, + string timeframe) + { + if (string.IsNullOrWhiteSpace(datasetName)) + { + throw new KeenException("A dataset name is required."); + } + + if (string.IsNullOrWhiteSpace(indexBy)) + { + throw new KeenException("A value to index by is required."); + } + + if (string.IsNullOrWhiteSpace(timeframe)) + { + throw new KeenException("A timeframe by is required."); + } + + if (string.IsNullOrWhiteSpace(_readKey)) + { + throw new KeenException("An API ReadKey is required to get dataset results."); + } + + var datasetResultsUrl = $"{GetDatasetUrl(datasetName)}/results"; + + // Absolute timeframes can have reserved characters like ':', and index_by can be + // any valid JSON member name, which can have all sorts of stuff, so we escape here. + var url = $"{datasetResultsUrl}?" + + $"index_by={Uri.EscapeDataString(indexBy)}" + + $"&timeframe={Uri.EscapeDataString(timeframe)}"; + + var responseMsg = await _keenHttpClient + .GetAsync(url, _readKey) + .ConfigureAwait(continueOnCapturedContext: false); + + var responseString = await responseMsg + .Content + .ReadAsStringAsync() + .ConfigureAwait(continueOnCapturedContext: false); + + var response = JObject.Parse(responseString); + + KeenUtil.CheckApiErrorCode(response); + + if (!responseMsg.IsSuccessStatusCode) + { + throw new KeenException($"Request failed with status: {responseMsg.StatusCode}"); + } + + return response; + } + + public async Task GetDefinitionAsync(string datasetName) + { + if (string.IsNullOrWhiteSpace(datasetName)) + { + throw new KeenException("A dataset name is required."); + } + + if (string.IsNullOrWhiteSpace(_readKey)) + { + throw new KeenException("An API ReadKey is required to get dataset results."); + } + + var responseMsg = await _keenHttpClient + .GetAsync(GetDatasetUrl(datasetName), _readKey) + .ConfigureAwait(continueOnCapturedContext: false); + + var responseString = await responseMsg + .Content + .ReadAsStringAsync() + .ConfigureAwait(continueOnCapturedContext: false); + + var response = JObject.Parse(responseString); + + KeenUtil.CheckApiErrorCode(response); + + if (!responseMsg.IsSuccessStatusCode) + { + throw new KeenException($"Request failed with status: {responseMsg.StatusCode}"); + } + + return JsonConvert.DeserializeObject(responseString, + SERIALIZER_SETTINGS); + } + + public async Task ListDefinitionsAsync( + int limit = 10, + string afterName = null) + { + if (string.IsNullOrWhiteSpace(_readKey)) + { + throw new KeenException("An API ReadKey is required to get dataset results."); + } + + // limit is just an int, so no need to encode here. + var datasetResultsUrl = $"{_cachedDatasetRelativeUrl}?limit={limit}"; + + if (!string.IsNullOrWhiteSpace(afterName)) + { + // afterName should be a valid dataset name, which can only be + // alphanumerics, '_' and '-', so we don't escape here. + datasetResultsUrl += $"&after_name={afterName}"; + } + + var responseMsg = await _keenHttpClient + .GetAsync(datasetResultsUrl, _readKey) + .ConfigureAwait(continueOnCapturedContext: false); + + var responseString = await responseMsg + .Content + .ReadAsStringAsync() + .ConfigureAwait(continueOnCapturedContext: false); + + var response = JObject.Parse(responseString); + + KeenUtil.CheckApiErrorCode(response); + + if (!responseMsg.IsSuccessStatusCode) + { + throw new KeenException($"Request failed with status: {responseMsg.StatusCode}"); + } + + return JsonConvert.DeserializeObject(responseString, + SERIALIZER_SETTINGS); + } + + public async Task> ListAllDefinitionsAsync() + { + var allDefinitions = new List(); + var firstSet = await ListDefinitionsAsync(MAX_DATASET_DEFINITION_LIST_LIMIT) + .ConfigureAwait(continueOnCapturedContext: false); + + if (null == firstSet?.Datasets) + { + throw new KeenException("Failed to fetch definition list"); + } + + if (!firstSet.Datasets.Any()) + { + return allDefinitions; + } + + if (firstSet.Count <= firstSet.Datasets.Count()) + { + return firstSet.Datasets; + } + + allDefinitions.AddRange(firstSet.Datasets); + + do + { + var nextSet = await ListDefinitionsAsync(MAX_DATASET_DEFINITION_LIST_LIMIT, + allDefinitions.Last().DatasetName) + .ConfigureAwait(continueOnCapturedContext: false); + + if (null == nextSet?.Datasets || !nextSet.Datasets.Any()) + { + throw new KeenException("Failed to fetch definition list"); + } + + allDefinitions.AddRange(nextSet.Datasets); + } while (firstSet.Count > allDefinitions.Count); + + return allDefinitions; + } + + public async Task DeleteDatasetAsync(string datasetName) + { + if (string.IsNullOrWhiteSpace(datasetName)) + { + throw new KeenException("A dataset name is required."); + } + + if (string.IsNullOrWhiteSpace(_masterKey)) + { + throw new KeenException("An API MasterKey is required to get dataset results."); + } + + var responseMsg = await _keenHttpClient + .DeleteAsync(GetDatasetUrl(datasetName), _masterKey) + .ConfigureAwait(continueOnCapturedContext: false); + + var responseString = await responseMsg + .Content + .ReadAsStringAsync() + .ConfigureAwait(continueOnCapturedContext: false); + + if (HttpStatusCode.NoContent != responseMsg.StatusCode) + { + var response = JObject.Parse(responseString); + + KeenUtil.CheckApiErrorCode(response); + + throw new KeenException($"Request failed with status: {responseMsg.StatusCode}"); + } + } + + public async Task CreateDatasetAsync(DatasetDefinition dataset) + { + if (string.IsNullOrWhiteSpace(_masterKey)) + { + throw new KeenException("An API MasterKey is required to get dataset results."); + } + + // Validate + if (null == dataset) + { + throw new KeenException("An instance of DatasetDefinition must be provided"); + } + + // This throws if dataset is not valid. + dataset.Validate(); + + var content = JsonConvert.SerializeObject(dataset, SERIALIZER_SETTINGS); + + var responseMsg = await _keenHttpClient + .PutAsync(GetDatasetUrl(dataset.DatasetName), _masterKey, content) + .ConfigureAwait(continueOnCapturedContext: false); + + var responseString = await responseMsg + .Content + .ReadAsStringAsync() + .ConfigureAwait(continueOnCapturedContext: false); + + var response = JObject.Parse(responseString); + + KeenUtil.CheckApiErrorCode(response); + + if (!responseMsg.IsSuccessStatusCode) + { + throw new KeenException($"Request failed with status: {responseMsg.StatusCode}"); + } + + return JsonConvert.DeserializeObject(responseString, + SERIALIZER_SETTINGS); + } + + private string GetDatasetUrl(string datasetName = null) + { + return $"{_cachedDatasetRelativeUrl}/{datasetName}"; + } + } +} diff --git a/Keen.NetStandard/Dataset/IDataset.cs b/Keen.NetStandard/Dataset/IDataset.cs new file mode 100644 index 0000000..e24b1f5 --- /dev/null +++ b/Keen.NetStandard/Dataset/IDataset.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; + + +namespace Keen.Core.Dataset +{ + public interface IDataset + { + /// + /// Get query results from a Cached Dataset. + /// + /// Name of cached dataset to query. + /// The string property value by which to retrieve results. + /// Limits retrieval of results to a specific portion of the + /// Cached Dataset + /// A JObject containing query results and metadata defining the cached + /// dataset. + Task GetResultsAsync(string datasetName, string indexBy, string timeframe); + + /// + /// Get the definition of your cached dataset. + /// + /// Name of cached dataset for which to retrieve the + /// definition. + /// An DatasetDefinition containing metadata about a cached dataset. + Task GetDefinitionAsync(string datasetName); + + /// + /// Lists the first n cached dataset definitions in your project. + /// + /// How many cached dataset definitions to return at a time (1-100). + /// Defaults to 10. + /// A cursor for use in pagination. afterName is the Cached Dataset + /// name that defines your place in the list. + Task ListDefinitionsAsync(int limit = 10, + string afterName = null); + + /// + /// Lists all the dataset definitions in the project. + /// + /// An enumerable of DatasetDefinitions. + Task> ListAllDefinitionsAsync(); + + /// + /// Delete a Cached Dataset + /// + /// The name of the dataset to be deleted. + Task DeleteDatasetAsync(string datasetName); + + /// + /// Creates a new Cached Dataset + /// + /// An instance of DatasetDefinition. At minimum, it must have + /// DatasetName, DisplayName, IndexBy and Query populated. + /// An instance of DatasetDefinition populated with more information about the + /// create Dataset. + Task CreateDatasetAsync(DatasetDefinition dataset); + } +} diff --git a/Keen.NetStandard/Dataset/QueryDefinitionExtensions.cs b/Keen.NetStandard/Dataset/QueryDefinitionExtensions.cs new file mode 100644 index 0000000..616d966 --- /dev/null +++ b/Keen.NetStandard/Dataset/QueryDefinitionExtensions.cs @@ -0,0 +1,31 @@ +using Keen.Core.Query; + + +namespace Keen.Core.Dataset +{ + internal static class QueryDefinitionExtensions + { + public static void ValidateForCachedDataset(this QueryDefinition query) + { + if (string.IsNullOrWhiteSpace(query.AnalysisType)) + { + throw new KeenException("QueryDefinition must have an analysis type"); + } + + if (string.IsNullOrWhiteSpace(query.EventCollection)) + { + throw new KeenException("QueryDefinition must specify an event collection"); + } + + if (string.IsNullOrWhiteSpace(query.Timeframe)) + { + throw new KeenException("QueryDefinition must specify a timeframe"); + } + + if (string.IsNullOrWhiteSpace(query.Interval)) + { + throw new KeenException("QueryDefinition must specify an interval"); + } + } + } +} diff --git a/Keen.NetStandard/IKeenHttpClient.cs b/Keen.NetStandard/IKeenHttpClient.cs index 88d3d83..812e07c 100644 --- a/Keen.NetStandard/IKeenHttpClient.cs +++ b/Keen.NetStandard/IKeenHttpClient.cs @@ -70,5 +70,27 @@ public interface IKeenHttpClient /// The key to use for authenticating this request. /// The response message. Task DeleteAsync(Uri resource, string authKey); + + /// + /// Create and send a PUT request with the given content to the given relative resource + /// using the given key for authentication. + /// + /// The relative resource to PUT. Must be properly formatted as a + /// relative Uri. + /// The key to use for authenticating this request. + /// The PUT body to send. + /// >The response message. + Task PutAsync(string resource, string authKey, string content); + + /// + /// Create and send a PUT request with the given content to the given relative resource + /// using the given key for authentication. + /// + /// The relative resource to PUT. Must be properly formatted as a + /// relative Uri. + /// The key to use for authenticating this request. + /// The PUT body to send. + /// >The response message. + Task PutAsync(Uri resource, string authKey, string content); } } diff --git a/Keen.NetStandard/KeenClient.cs b/Keen.NetStandard/KeenClient.cs index 2391edc..2c8ed91 100644 --- a/Keen.NetStandard/KeenClient.cs +++ b/Keen.NetStandard/KeenClient.cs @@ -1,4 +1,5 @@ using Keen.Core.DataEnrichment; +using Keen.Core.Dataset; using Keen.Core.EventCache; using Keen.Core.Query; using Newtonsoft.Json.Linq; @@ -48,6 +49,8 @@ public class KeenClient /// public IQueries Queries { get; set; } + public IDataset Datasets { get; set; } + /// /// Add a static global property. This property will be added to /// every event. @@ -116,6 +119,7 @@ private KeenClient(IProjectSettings prjSettings, EventCollection = new EventCollection(_prjSettings, keenHttpClientProvider); Event = new Event(_prjSettings, keenHttpClientProvider); Queries = new Queries(_prjSettings, keenHttpClientProvider); + Datasets = new Datasets(_prjSettings, keenHttpClientProvider); } /// @@ -148,15 +152,14 @@ public KeenClient(IProjectSettings prjSettings, /// Master API key is required. /// /// Name of collection to delete. - public async Task DeleteCollectionAsync(string collection) + public Task DeleteCollectionAsync(string collection) { // Preconditions KeenUtil.ValidateEventCollectionName(collection); if (string.IsNullOrWhiteSpace(_prjSettings.MasterKey)) throw new KeenException("Master API key is required for DeleteCollection"); - await EventCollection.DeleteCollection(collection) - .ConfigureAwait(continueOnCapturedContext: false); + return EventCollection.DeleteCollection(collection); } /// @@ -180,14 +183,13 @@ public void DeleteCollection(string collection) /// Return schema information for all the event collections in this project. /// /// - public async Task GetSchemasAsync() + public Task GetSchemasAsync() { // Preconditions if (string.IsNullOrWhiteSpace(_prjSettings.ReadKey)) throw new KeenException("Read API key is required for GetSchemas"); - return await Event.GetSchemas() - .ConfigureAwait(continueOnCapturedContext: false); + return Event.GetSchemas(); } /// @@ -238,7 +240,7 @@ public dynamic GetSchema(string collection) throw ex.TryUnwrap(); } } - + /// /// Insert multiple events in a single request. /// @@ -257,7 +259,6 @@ public void AddEvents(string collection, IEnumerable eventsInfo, IEnumer } } - /// /// Add a collection of events to the specified collection. Assumes that /// objects in the collection have already been through AddEvent to receive @@ -274,11 +275,10 @@ private async Task> AddEventsBulkAsync(string collectio if (!eventsInfo.Any()) return new List(); // Build a container object with a property to identify the collection - var jEvent = new JObject { { collection, JToken.FromObject(eventsInfo) } }; + var jEvent = new JObject {{collection, JToken.FromObject(eventsInfo)}}; // Use the bulk interface to add events - return await Event.AddEvents(jEvent) - .ConfigureAwait(false); + return await Event.AddEvents(jEvent).ConfigureAwait(false); } /// @@ -349,7 +349,7 @@ await EventCache.AddAsync(new CachedEvent(collection, jEvent)) await EventCollection.AddEvent(collection, jEvent) .ConfigureAwait(false); } - + /// /// Convert a user-supplied object to a JObject that can be sent to the Keen.IO API. /// @@ -385,12 +385,12 @@ private JObject PrepareUserObject(object eventInfo, IEnumerable addOns) // Ensure this event has a 'keen' object of the correct type if (null == jEvent.Property("keen")) jEvent.Add("keen", new JObject()); - else if (jEvent.Property("keen").Value.GetType() != typeof(JObject)) + else if (jEvent.Property("keen").Value.GetType() != typeof (JObject)) throw new KeenException(string.Format("Value of property \"keen\" must be an object, is {0}", jEvent.Property("keen").GetType())); - var keen = ((JObject)jEvent.Property("keen").Value); + var keen = ((JObject) jEvent.Property("keen").Value); if (addOns != null && addOns.Any()) keen.Add("addons", JArray.FromObject(addOns)); @@ -486,14 +486,13 @@ public async Task SendCachedEventsAsync() throw new KeenBulkException("One or more cached events could not be submitted", failedEvents); } - /// /// Retrieve a list of all the queries supported by the API. /// /// - public async Task>> GetQueries() + public Task>> GetQueriesAsync() { - return await Queries.AvailableQueries(); + return Queries.AvailableQueries(); } /// @@ -502,9 +501,9 @@ public async Task>> GetQueries() /// /// /// - public async Task QueryAsync(string queryName, Dictionary parms) + public Task QueryAsync(string queryName, Dictionary parms) { - return await Queries.Metric(queryName, parms); + return Queries.Metric(queryName, parms); } /// @@ -536,13 +535,10 @@ public JObject Query(string queryName, Dictionary parms) /// Filter to narrow down the events used in analysis. Optional, may be null. /// The timezone to use when specifying a relative timeframe. Optional, may be blank. /// - public async Task QueryAsync(QueryType queryType, string collection, string targetProperty, + public Task QueryAsync(QueryType queryType, string collection, string targetProperty, IQueryTimeframe timeframe = null, IEnumerable filters = null, string timezone = "") { - return - await - Queries.Metric(queryType, collection, targetProperty, timeframe, filters, timezone) - .ConfigureAwait(false); + return Queries.Metric(queryType, collection, targetProperty, timeframe, filters, timezone); } /// @@ -579,14 +575,11 @@ public string Query(QueryType queryType, string collection, string targetPropert /// Filter to narrow down the events used in analysis. Optional, may be null. /// The timezone to use when specifying a relative timeframe. Optional, may be blank. /// - public async Task>> QueryGroupAsync(QueryType queryType, string collection, + public Task>> QueryGroupAsync(QueryType queryType, string collection, string targetProperty, string groupBy, IQueryTimeframe timeframe = null, IEnumerable filters = null, string timezone = "") { - return - await - Queries.Metric(queryType, collection, targetProperty, groupBy, timeframe, filters, timezone) - .ConfigureAwait(false); + return Queries.Metric(queryType, collection, targetProperty, groupBy, timeframe, filters, timezone); } /// @@ -626,14 +619,11 @@ public IEnumerable> QueryGroup(QueryType queryType, stri /// Filters to narrow down the events used in analysis. Optional, may be null. /// The timezone to use when specifying a relative timeframe. Optional, may be blank. /// - public async Task>> QueryIntervalAsync(QueryType queryType, + public Task>> QueryIntervalAsync(QueryType queryType, string collection, string targetProperty, IQueryTimeframe timeframe, QueryInterval interval = null, IEnumerable filters = null, string timezone = "") { - return - await - Queries.Metric(queryType, collection, targetProperty, timeframe, interval, filters, timezone) - .ConfigureAwait(false); + return Queries.Metric(queryType, collection, targetProperty, timeframe, interval, filters, timezone); } /// @@ -675,14 +665,11 @@ public IEnumerable> QueryInterval(QueryType queryType /// Filters to narrow down the events used in analysis. Optional, may be null. /// The timezone to use when specifying a relative timeframe. Optional, may be blank. /// - public async Task>>>> QueryIntervalGroupAsync + public Task>>>> QueryIntervalGroupAsync (QueryType queryType, string collection, string targetProperty, string groupBy, IQueryTimeframe timeframe, QueryInterval interval, IEnumerable filters = null, string timezone = "") { - return - await - Queries.Metric(queryType, collection, targetProperty, groupBy, timeframe, interval, filters, - timezone).ConfigureAwait(false); + return Queries.Metric(queryType, collection, targetProperty, groupBy, timeframe, interval, filters, timezone); } /// @@ -722,10 +709,10 @@ public IEnumerable>>> Que /// Request up to 100 of the most recent events added to a given collection. /// If specified, email will be sent when the data is ready for download. Otherwise, it will be returned directly. /// - public async Task> QueryExtractResourceAsync(string collection, + public Task> QueryExtractResourceAsync(string collection, IQueryTimeframe timeframe = null, IEnumerable filters = null, int latest = 0, string email = "") { - return await Queries.Extract(collection, timeframe, filters, latest, email).ConfigureAwait(false); + return Queries.Extract(collection, timeframe, filters, latest, email); } /// @@ -757,10 +744,10 @@ public IEnumerable QueryExtractResource(string collection, IQueryTimefr /// Specifies window of time from which to select events for analysis. May be absolute or relative. /// The timezone to use when specifying a relative timeframe. Optional, may be blank. /// - public async Task QueryFunnelAsync(IEnumerable steps, + public Task QueryFunnelAsync(IEnumerable steps, IQueryTimeframe timeframe = null, string timezone = "") { - return await Queries.Funnel(steps, timeframe, timezone).ConfigureAwait(false); + return Queries.Funnel(steps, timeframe, timezone); } /// @@ -792,14 +779,11 @@ public FunnelResult QueryFunnel(IEnumerable steps, /// Filter to narrow down the events used in analysis. Optional, may be null. /// The timezone to use when specifying a relative timeframe. Optional, may be blank. /// - public async Task> QueryMultiAnalysisAsync(string collection, + public Task> QueryMultiAnalysisAsync(string collection, IEnumerable analysisParams, IQueryTimeframe timeframe = null, IEnumerable filters = null, string timezone = "") { - return - await - Queries.MultiAnalysis(collection, analysisParams, timeframe, filters, timezone) - .ConfigureAwait(false); + return Queries.MultiAnalysis(collection, analysisParams, timeframe, filters, timezone); } /// @@ -836,14 +820,11 @@ public IDictionary QueryMultiAnalysis(string collection, /// Name of a collection field by which to group results. /// The timezone to use when specifying a relative timeframe. Optional, may be blank. /// - public async Task>>> QueryMultiAnalysisGroupAsync( + public Task>>> QueryMultiAnalysisGroupAsync( string collection, IEnumerable analysisParams, IQueryTimeframe timeframe = null, IEnumerable filters = null, string groupBy = "", string timezone = "") { - return - await - Queries.MultiAnalysis(collection, analysisParams, timeframe, filters, groupBy, timezone) - .ConfigureAwait(false); + return Queries.MultiAnalysis(collection, analysisParams, timeframe, filters, groupBy, timezone); } /// @@ -884,14 +865,11 @@ public IEnumerable>> QueryMultiAnaly /// Filters to narrow down the events used in analysis. Optional, may be null. /// The timezone to use when specifying a relative timeframe. Optional, may be blank. /// - public async Task>>> QueryMultiAnalysisIntervalAsync( + public Task>>> QueryMultiAnalysisIntervalAsync( string collection, IEnumerable analysisParams, IQueryTimeframe timeframe = null, QueryInterval interval = null, IEnumerable filters = null, string timezone = "") { - return - await - Queries.MultiAnalysis(collection, analysisParams, timeframe, interval, filters, timezone) - .ConfigureAwait(false); + return Queries.MultiAnalysis(collection, analysisParams, timeframe, interval, filters, timezone); } /// @@ -933,15 +911,12 @@ public IEnumerable>> QueryMultiAn /// Filters to narrow down the events used in analysis. Optional, may be null. /// The timezone to use when specifying a relative timeframe. Optional, may be blank. /// - public async Task>>>>> + public Task>>>>> QueryMultiAnalysisIntervalGroupAsync(string collection, IEnumerable analysisParams, IQueryTimeframe timeframe = null, QueryInterval interval = null, IEnumerable filters = null, string groupBy = "", string timezone = "") { - return - await - Queries.MultiAnalysis(collection, analysisParams, timeframe, interval, filters, groupBy, timezone) - .ConfigureAwait(false); + return Queries.MultiAnalysis(collection, analysisParams, timeframe, interval, filters, groupBy, timezone); } /// @@ -972,5 +947,179 @@ public IEnumerable + /// Get query results from a Cached Dataset. + /// + /// Name of cached dataset to query. + /// The string property value you want to retrieve results by. + /// Limits retrieval of results to a specific portion of the Cached Dataset + /// An instance of Newtonsoft.Json.Linq.JObject containing query results and metadata defining the cached dataset. + public Task QueryDatasetAsync(string datasetName, string indexBy, string timeframe) + { + return Datasets.GetResultsAsync(datasetName, indexBy, timeframe); + } + + /// + /// Get query results from a Cached Dataset. + /// + /// Name of cached dataset to query. + /// The string property value you want to retrieve results by. + /// Limits retrieval of results to a specific portion of the Cached Dataset + /// An instance of Newtonsoft.Json.Linq.JObject containing query results and metadata defining the cached dataset. + public JObject QueryDataset(string datasetName, string indexBy, string timeframe) + { + // NOTE : The docs seem to indicate you can retrieve based on an absolute timeframe, + // so maybe we should be accepting an IQueryTimeframe instance instead of a string. + try + { + return QueryDatasetAsync(datasetName, indexBy, timeframe).Result; + } + catch (AggregateException ex) + { + throw ex.TryUnwrap(); + } + } + + /// + /// Get the definition of your cached dataset. + /// + /// Name of cached dataset to get the definition of. + /// An instance of Keen.Core.Dataset.DatasetDefinition containing metadata about your cached dataset. + public Task GetDatasetDefinitionAsync(string datasetName) + { + return Datasets.GetDefinitionAsync(datasetName); + } + + /// + /// Get the definition of your cached dataset. + /// + /// Name of cached dataset to get the definition of. + /// An instance of Keen.Core.Dataset.DatasetDefinition containing metadata about your cached dataset. + public DatasetDefinition GetDatasetDefinition(string datasetName) + { + try + { + return GetDatasetDefinitionAsync(datasetName).Result; + } + catch (AggregateException ex) + { + throw ex.TryUnwrap(); + } + } + + /// + /// Lists the first n cached dataset definitions in your project. + /// + /// How many cached dataset definitions to return at a time (1-100). Defaults to 10. + /// A cursor for use in pagination. afterName is the Cached Dataset name that defines your place in the list. + /// For instance, if you make a list request and receive 100 Cached Dataset definitions, + /// ending with dataset_foo you can use dataset_foo as your afterName to retrieve the next page of definitions. + /// Lists also return with helper “NextPageUrl” that uses AfterName, + /// so your subsequent call can fetch the next page of the list easily. + /// An instance of Keen.Core.Dataset.DatasetDefinitionCollection containing the total count, next page url and list of DatasetDefinitions. + public Task ListDatasetDefinitionsAsync(int limit = 10, string afterName = null) + { + return Datasets.ListDefinitionsAsync(limit, afterName); + } + + /// + /// Lists the first n cached dataset definitions in your project. + /// + /// How many cached dataset definitions to return at a time (1-100). Defaults to 10. + /// A cursor for use in pagination. afterName is the Cached Dataset name that defines your place in the list. + /// For instance, if you make a list request and receive 100 Cached Dataset definitions, + /// ending with dataset_foo you can use dataset_foo as your afterName to retrieve the next page of definitions. + /// Lists also return with helper “NextPageUrl” that uses AfterName, + /// so your subsequent call can fetch the next page of the list easily. + /// An instance of Keen.Core.Dataset.DatasetDefinitionCollection containing the total count, next page url and list of DatasetDefinitions. + public DatasetDefinitionCollection ListDatasetDefinitions(int limit = 10, string afterName = null) + { + try + { + return ListDatasetDefinitionsAsync(limit, afterName).Result; + } + catch(AggregateException ex) + { + throw ex.TryUnwrap(); + } + } + + /// + /// Lists all the dataset definitions in your project. + /// + /// A list of Keen.Core.Dataset.DatasetDefinition + public Task> ListAllDatasetDefinitionsAsync() + { + return Datasets.ListAllDefinitionsAsync(); + } + + /// + /// Lists all the dataset definitions in your project. + /// + /// A list of Keen.Core.Dataset.DatasetDefinition + public IEnumerable ListAllDatasetDefinitions() + { + try + { + return ListAllDatasetDefinitionsAsync().Result; + } + catch (AggregateException ex) + { + throw ex.TryUnwrap(); + } + } + + /// + /// Creates a new Cached Dataset + /// + /// An instance of Keen.Core.Dataset.DatasetDefinition. It must have DatasetName, DisplayName, IndexBy and Query populated. + /// An instance of Keen.Core.Dataset.DatasetDefinition populated more information. + public Task CreateDatasetAsync(DatasetDefinition dataset) + { + return Datasets.CreateDatasetAsync(dataset); + } + + /// + /// Creates a new Cached Dataset + /// + /// An instance of Keen.Core.Dataset.DatasetDefinition. It must have DatasetName, DisplayName, IndexBy and Query populated. + /// An instance of Keen.Core.Dataset.DatasetDefinition populated more information. + public DatasetDefinition CreateDataset(DatasetDefinition dataset) + { + try + { + return CreateDatasetAsync(dataset).Result; + } + catch (AggregateException ex) + { + throw ex.TryUnwrap(); + } + } + + /// + /// Delete a Cached Dataset + /// + /// The name of the dataset to be deleted. + public Task DeleteDatasetAsync(string datasetName) + { + return Datasets.DeleteDatasetAsync(datasetName); + } + + /// + /// Delete a Cached Dataset + /// + /// The name of the dataset to be deleted. + public void DeleteDataset(string datasetName) + { + try + { + DeleteDatasetAsync(datasetName).Wait(); + } + catch (AggregateException ex) + { + throw ex.TryUnwrap(); + } + } } -} \ No newline at end of file +} diff --git a/Keen.NetStandard/KeenConstants.cs b/Keen.NetStandard/KeenConstants.cs index 4fe2c18..0b1c6a4 100644 --- a/Keen.NetStandard/KeenConstants.cs +++ b/Keen.NetStandard/KeenConstants.cs @@ -81,6 +81,9 @@ public class KeenConstants private const int bulkBatchSize = 1000; public static int BulkBatchSize { get { return bulkBatchSize; } protected set { ;} } + private const string datasetsResource = "datasets"; + public static string DatasetsResource { get { return datasetsResource; } protected set { ;} } + private const string keenProjectId = "KEEN_PROJECT_ID"; public static string KeenProjectId { get { return keenProjectId; } protected set {; } } diff --git a/Keen.NetStandard/KeenHttpClient.cs b/Keen.NetStandard/KeenHttpClient.cs index 7a6eedf..4ebccbf 100644 --- a/Keen.NetStandard/KeenHttpClient.cs +++ b/Keen.NetStandard/KeenHttpClient.cs @@ -43,7 +43,7 @@ internal static string GetRelativeUrl(string projectId, string resource) /// The relative resource to GET. Must be properly formatted as a /// relative Uri. /// The key to use for authenticating this request. - /// >The response message. + /// The response message. public Task GetAsync(string resource, string authKey) { var url = new Uri(resource, UriKind.Relative); @@ -57,7 +57,7 @@ public Task GetAsync(string resource, string authKey) /// /// The relative resource to GET. /// The key to use for authenticating this request. - /// >The response message. + /// The response message. public Task GetAsync(Uri resource, string authKey) { KeenHttpClient.RequireAuthKey(authKey); @@ -78,7 +78,7 @@ public Task GetAsync(Uri resource, string authKey) /// relative Uri. /// The key to use for authenticating this request. /// The POST body to send. - /// >The response message. + /// The response message. public Task PostAsync(string resource, string authKey, string content) { var url = new Uri(resource, UriKind.Relative); @@ -93,44 +93,12 @@ public Task PostAsync(string resource, string authKey, stri /// The relative resource to POST. /// The key to use for authenticating this request. /// The POST body to send. - /// >The response message. - public async Task PostAsync(Uri resource, - string authKey, - string content) + /// The response message. + public Task PostAsync(Uri resource, + string authKey, + string content) { - KeenHttpClient.RequireAuthKey(authKey); - - if (string.IsNullOrWhiteSpace(content)) - { - // Technically, we can encode an empty string or whitespace, but why? For now - // we use GET for querying. If we ever need to POST with no content, we should - // reorganize the logic below to never create/set the content stream. - throw new ArgumentNullException(nameof(content), "Unexpected empty content."); - } - - // If we switch PCL profiles, instead use MediaTypeFormatters (or ObjectContent)?, - // like here?: https://msdn.microsoft.com/en-us/library/system.net.http.httpclientextensions.putasjsonasync(v=vs.118).aspx - using (var contentStream = - new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(content)))) - { - // TODO : Amake sure this is the same as Add("content-type", "application/json") - contentStream.Headers.ContentType = - new MediaTypeHeaderValue(KeenHttpClient.JSON_CONTENT_TYPE); - - HttpRequestMessage post = CreateRequest(HttpMethod.Post, resource, authKey); - post.Content = contentStream; - - return await _httpClient.SendAsync(post).ConfigureAwait(false); - - // TODO : Should we do the KeenUtil.CheckApiErrorCode() here? - // TODO : Should we check the if (!responseMsg.IsSuccessStatusCode) here too? - // TODO : If we centralize error checking in this class we could have variations - // of these helpers that return string or JToken or JArray or JObject. It might - // also be nice for those options to optionally hand back the raw - // HttpResponseMessage in an out param if desired? - // TODO : Use CallerMemberNameAttribute to print error messages? - // http://stackoverflow.com/questions/3095696/how-do-i-get-the-calling-method-name-and-type-using-reflection?noredirect=1&lq=1 - } + return DispatchWithContentAsync(HttpMethod.Post, resource, authKey, content); } /// @@ -164,6 +132,78 @@ public Task DeleteAsync(Uri resource, string authKey) return _httpClient.SendAsync(delete); } + /// + /// Create and send a PUT request with the given content to the given relative resource + /// using the given key for authentication. + /// + /// The relative resource to PUT. Must be properly formatted as a + /// relative Uri. + /// The key to use for authenticating this request. + /// The PUT body to send. + /// The response message. + public Task PutAsync(string resource, string authKey, string content) + { + var url = new Uri(resource, UriKind.Relative); + + return PutAsync(url, authKey, content); + } + + /// + /// Create and send a PUT request with the given content to the given relative resource + /// using the given key for authentication. + /// + /// The relative resource to PUT. Must be properly formatted as a + /// relative Uri. + /// The key to use for authenticating this request. + /// The PUT body to send. + /// The response message. + public Task PutAsync(Uri resource, + string authKey, + string content) + { + return DispatchWithContentAsync(HttpMethod.Put, resource, authKey, content); + } + + private async Task DispatchWithContentAsync(HttpMethod httpMethod, + Uri resource, + string authKey, + string content) + { + KeenHttpClient.RequireAuthKey(authKey); + + if (string.IsNullOrWhiteSpace(content)) + { + // Technically, we can encode an empty string or whitespace, but why? For now + // we use GET for querying. If we ever need to POST with no content, we should + // reorganize the logic below to never create/set the content stream. + throw new ArgumentNullException(nameof(content), "Unexpected empty content."); + } + + // If we switch PCL profiles, instead use MediaTypeFormatters (or ObjectContent)?, + // like here?: https://msdn.microsoft.com/en-us/library/system.net.http.httpclientextensions.putasjsonasync(v=vs.118).aspx + using (var contentStream = + new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(content)))) + { + // TODO : Make sure this is the same as Add("content-type", "application/json") + contentStream.Headers.ContentType = + new MediaTypeHeaderValue(KeenHttpClient.JSON_CONTENT_TYPE); + + HttpRequestMessage request = CreateRequest(httpMethod, resource, authKey); + request.Content = contentStream; + + return await _httpClient.SendAsync(request).ConfigureAwait(false); + + // TODO : Should we do the KeenUtil.CheckApiErrorCode() here? + // TODO : Should we check the if (!responseMsg.IsSuccessStatusCode) here too? + // TODO : If we centralize error checking in this class we could have variations + // of these helpers that return string or JToken or JArray or JObject. It might + // also be nice for those options to optionally hand back the raw + // HttpResponseMessage in an out param if desired? + // TODO : Use CallerMemberNameAttribute to print error messages? + // http://stackoverflow.com/questions/3095696/how-do-i-get-the-calling-method-name-and-type-using-reflection?noredirect=1&lq=1 + } + } + private static HttpRequestMessage CreateRequest(HttpMethod verb, Uri resource, string authKey) diff --git a/Keen.NetStandard/Query/QueryDefinition.cs b/Keen.NetStandard/Query/QueryDefinition.cs new file mode 100644 index 0000000..49edecb --- /dev/null +++ b/Keen.NetStandard/Query/QueryDefinition.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; + + +namespace Keen.Core.Query +{ + /// + /// Holds information describing the query that is cached within a cached dataset. + /// + public class QueryDefinition + { + /// + /// Unique id of the project to analyze. + /// + public string ProjectId { get; set; } + + /// + /// The type of analysis for this query (e.g. count, count_unique, sum etc.) + /// + public string AnalysisType { get; set; } + + /// + /// Specifies the name of the event collection to analyze. + /// + public string EventCollection { get; set; } + + /// + /// Refines the scope of events to be included in the analysis based on event property + /// values. + /// + public IEnumerable Filters { get; set; } + + /// + /// Limits analysis to a specific period of time when the events occurred. + /// + public string Timeframe { get; set; } + + /// + /// Assigns a timezone offset to relative timeframes. + /// + public string Timezone { get; set; } + + /// + /// Specifies the size of time interval by which to group results. Using this parameter + /// changes the response format. + /// + public string Interval { get; set; } + + /// + /// Specifies the names of properties by which to group results. Using this parameter + /// changes the response format. + /// + public IEnumerable GroupBy { get; set; } + } +} diff --git a/Keen/Dataset/DatasetDefinition.cs b/Keen/Dataset/DatasetDefinition.cs new file mode 100644 index 0000000..4439346 --- /dev/null +++ b/Keen/Dataset/DatasetDefinition.cs @@ -0,0 +1,76 @@ +using Keen.Core.Query; +using System; +using System.Collections.Generic; +using System.Linq; + + +namespace Keen.Core.Dataset +{ + public class DatasetDefinition + { + /// + /// Name of the dataset, which is used as an identifier. Must be unique per project. + /// + public string DatasetName { get; set; } + + /// + /// The human-readable string name for your Cached Dataset. + /// + public string DisplayName { get; set; } + + /// + /// Holds information describing the query which is cached by this Cached Dataset. + /// + public QueryDefinition Query { get; set; } + + /// + /// When the most recent computation was queued. + /// + public DateTime? LastScheduledDate { get; set; } + + /// + /// The most recent interval that has been computed for the Cached Dataset. + /// + public DateTime? LatestSubtimeframeAvailable { get; set; } + + /// + /// The difference between now and the most recent datapoint computed. + /// + public long MillisecondsBehind { get; set; } + + /// + /// The event property name of string values results are retrieved by. + /// + public IEnumerable IndexBy { get; set; } + } + + internal static class DatasetDefinitionExtensions + { + public static void Validate(this DatasetDefinition dataset) + { + if (string.IsNullOrWhiteSpace(dataset.DatasetName)) + { + throw new KeenException("DatasetDefinition must have a name."); + } + + if (string.IsNullOrWhiteSpace(dataset.DisplayName)) + { + throw new KeenException("DatasetDefinition must have a display name."); + } + + if (null == dataset.IndexBy || + string.IsNullOrWhiteSpace(dataset.IndexBy.FirstOrDefault())) + { + throw new KeenException("DatasetDefinition must specify a property by which to " + + "index."); + } + + if (null == dataset.Query) + { + throw new KeenException("DatasetDefinition must contain a query to be cached."); + } + + dataset.Query.ValidateForCachedDataset(); + } + } +} diff --git a/Keen/Dataset/DatasetDefinitionCollection.cs b/Keen/Dataset/DatasetDefinitionCollection.cs new file mode 100644 index 0000000..adf6a10 --- /dev/null +++ b/Keen/Dataset/DatasetDefinitionCollection.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + + +namespace Keen.Core.Dataset +{ + /// + /// A model for a collection of DatasetDefinitions with paging information. + /// + public class DatasetDefinitionCollection + { + /// + /// A list of the DatasetDefinitions returned in this page. + /// + public IEnumerable Datasets { get; set; } + + /// + /// The url of the next page of Dataset definitions. + /// + public string NextPageUrl { get; set; } + + /// + /// The total count of Cached Datasets for this project. + /// + public int Count { get; set; } + } +} diff --git a/Keen/Dataset/Datasets.cs b/Keen/Dataset/Datasets.cs new file mode 100644 index 0000000..6fc9ddb --- /dev/null +++ b/Keen/Dataset/Datasets.cs @@ -0,0 +1,306 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + + +namespace Keen.Core.Dataset +{ + /// + /// Datasets implements the IDataset interface which represents the Keen.IO Cached Datasets + /// API methods. + /// + internal class Datasets : IDataset + { + private const int MAX_DATASET_DEFINITION_LIST_LIMIT = 100; + private static readonly JsonSerializerSettings SERIALIZER_SETTINGS = + new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }, + DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, + Formatting = Formatting.None + }; + + private readonly IKeenHttpClient _keenHttpClient; + private readonly string _cachedDatasetRelativeUrl; + private readonly string _masterKey; + + + internal Datasets(IProjectSettings prjSettings, + IKeenHttpClientProvider keenHttpClientProvider) + { + if (null == prjSettings) + { + throw new ArgumentNullException(nameof(prjSettings), + "Project Settings must be provided."); + } + + if (null == keenHttpClientProvider) + { + throw new ArgumentNullException(nameof(keenHttpClientProvider), + "A KeenHttpClient provider must be provided."); + } + + if (string.IsNullOrWhiteSpace(prjSettings.KeenUrl) || + !Uri.IsWellFormedUriString(prjSettings.KeenUrl, UriKind.Absolute)) + { + throw new KeenException( + "A properly formatted KeenUrl must be provided via Project Settings."); + } + + var serverBaseUrl = new Uri(prjSettings.KeenUrl); + _keenHttpClient = keenHttpClientProvider.GetForUrl(serverBaseUrl); + _cachedDatasetRelativeUrl = + KeenHttpClient.GetRelativeUrl(prjSettings.ProjectId, + KeenConstants.DatasetsResource); + + _masterKey = prjSettings.MasterKey; + } + + public async Task GetResultsAsync(string datasetName, + string indexBy, + string timeframe) + { + if (string.IsNullOrWhiteSpace(datasetName)) + { + throw new KeenException("A dataset name is required."); + } + + if (string.IsNullOrWhiteSpace(indexBy)) + { + throw new KeenException("A value to index by is required."); + } + + if (string.IsNullOrWhiteSpace(timeframe)) + { + throw new KeenException("A timeframe by is required."); + } + + if (string.IsNullOrWhiteSpace(_masterKey)) + { + throw new KeenException("An API masterkey is required to get dataset results."); + } + + var datasetResultsUrl = $"{GetDatasetUrl(datasetName)}/results"; + + var url = $"{datasetResultsUrl}?index_by={indexBy}&timeframe={timeframe}"; + + var responseMsg = await _keenHttpClient + .GetAsync(url, _masterKey) + .ConfigureAwait(continueOnCapturedContext: false); + + var responseString = await responseMsg + .Content + .ReadAsStringAsync() + .ConfigureAwait(continueOnCapturedContext: false); + + var response = JObject.Parse(responseString); + + KeenUtil.CheckApiErrorCode(response); + + if (!responseMsg.IsSuccessStatusCode) + { + throw new KeenException($"Request failed with status: {responseMsg.StatusCode}"); + } + + return response; + } + + public async Task GetDefinitionAsync(string datasetName) + { + if (string.IsNullOrWhiteSpace(datasetName)) + { + throw new KeenException("A dataset name is required."); + } + + if (string.IsNullOrWhiteSpace(_masterKey)) + { + throw new KeenException("An API masterkey is required to get dataset results."); + } + + var responseMsg = await _keenHttpClient + .GetAsync(GetDatasetUrl(datasetName), _masterKey) + .ConfigureAwait(continueOnCapturedContext: false); + + var responseString = await responseMsg + .Content + .ReadAsStringAsync() + .ConfigureAwait(continueOnCapturedContext: false); + + var response = JObject.Parse(responseString); + + KeenUtil.CheckApiErrorCode(response); + + if (!responseMsg.IsSuccessStatusCode) + { + throw new KeenException($"Request failed with status: {responseMsg.StatusCode}"); + } + + return JsonConvert.DeserializeObject(responseString, + SERIALIZER_SETTINGS); + } + + public async Task ListDefinitionsAsync( + int limit = 10, + string afterName = null) + { + if (string.IsNullOrWhiteSpace(_masterKey)) + { + throw new KeenException("An API masterkey is required to get dataset results."); + } + + var datasetResultsUrl = $"{_cachedDatasetRelativeUrl}?limit={limit}"; + + if (!string.IsNullOrWhiteSpace(afterName)) + { + datasetResultsUrl += $"&after_name={afterName}"; + } + + var responseMsg = await _keenHttpClient + .GetAsync(datasetResultsUrl, _masterKey) + .ConfigureAwait(continueOnCapturedContext: false); + + var responseString = await responseMsg + .Content + .ReadAsStringAsync() + .ConfigureAwait(continueOnCapturedContext: false); + + var response = JObject.Parse(responseString); + + KeenUtil.CheckApiErrorCode(response); + + if (!responseMsg.IsSuccessStatusCode) + { + throw new KeenException($"Request failed with status: {responseMsg.StatusCode}"); + } + + return JsonConvert.DeserializeObject(responseString, + SERIALIZER_SETTINGS); + } + + public async Task> ListAllDefinitionsAsync() + { + var allDefinitions = new List(); + var firstSet = await ListDefinitionsAsync(MAX_DATASET_DEFINITION_LIST_LIMIT) + .ConfigureAwait(continueOnCapturedContext: false); + + if (null == firstSet?.Datasets) + { + throw new KeenException("Failed to fetch definition list"); + } + + if (!firstSet.Datasets.Any()) + { + return allDefinitions; + } + + if (firstSet.Count <= firstSet.Datasets.Count()) + { + return firstSet.Datasets; + } + + allDefinitions.AddRange(firstSet.Datasets); + + do + { + var nextSet = await ListDefinitionsAsync(MAX_DATASET_DEFINITION_LIST_LIMIT, + allDefinitions.Last().DatasetName) + .ConfigureAwait(continueOnCapturedContext: false); + + if (null == nextSet?.Datasets || !nextSet.Datasets.Any()) + { + throw new KeenException("Failed to fetch definition list"); + } + + allDefinitions.AddRange(nextSet.Datasets); + } while (firstSet.Count > allDefinitions.Count); + + return allDefinitions; + } + + public async Task DeleteDatasetAsync(string datasetName) + { + if (string.IsNullOrWhiteSpace(datasetName)) + { + throw new KeenException("A dataset name is required."); + } + + if (string.IsNullOrWhiteSpace(_masterKey)) + { + throw new KeenException("An API masterkey is required to get dataset results."); + } + + var responseMsg = await _keenHttpClient + .DeleteAsync(GetDatasetUrl(datasetName), _masterKey) + .ConfigureAwait(continueOnCapturedContext: false); + + var responseString = await responseMsg + .Content + .ReadAsStringAsync() + .ConfigureAwait(continueOnCapturedContext: false); + + if (HttpStatusCode.NoContent == responseMsg.StatusCode) + { + return; + } + + var response = JObject.Parse(responseString); + + KeenUtil.CheckApiErrorCode(response); + + throw new KeenException($"Request failed with status: {responseMsg.StatusCode}"); + } + + public async Task CreateDatasetAsync(DatasetDefinition dataset) + { + if (string.IsNullOrWhiteSpace(_masterKey)) + { + throw new KeenException("An API masterkey is required to get dataset results."); + } + + // Validate + if (null == dataset) + { + throw new KeenException("An instance of DatasetDefinition must be provided"); + } + + // This throws if dataset is not valid. + dataset.Validate(); + + var content = JsonConvert.SerializeObject(dataset, SERIALIZER_SETTINGS); + + var responseMsg = await _keenHttpClient + .PutAsync(GetDatasetUrl(dataset.DatasetName), _masterKey, content) + .ConfigureAwait(continueOnCapturedContext: false); + + var responseString = await responseMsg + .Content + .ReadAsStringAsync() + .ConfigureAwait(continueOnCapturedContext: false); + + var response = JObject.Parse(responseString); + + KeenUtil.CheckApiErrorCode(response); + + if (!responseMsg.IsSuccessStatusCode) + { + throw new KeenException($"Request failed with status: {responseMsg.StatusCode}"); + } + + return JsonConvert.DeserializeObject(responseString, + SERIALIZER_SETTINGS); + } + + private string GetDatasetUrl(string datasetName = null) + { + return $"{_cachedDatasetRelativeUrl}/{datasetName}"; + } + } +} diff --git a/Keen/Dataset/IDataset.cs b/Keen/Dataset/IDataset.cs new file mode 100644 index 0000000..e24b1f5 --- /dev/null +++ b/Keen/Dataset/IDataset.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; + + +namespace Keen.Core.Dataset +{ + public interface IDataset + { + /// + /// Get query results from a Cached Dataset. + /// + /// Name of cached dataset to query. + /// The string property value by which to retrieve results. + /// Limits retrieval of results to a specific portion of the + /// Cached Dataset + /// A JObject containing query results and metadata defining the cached + /// dataset. + Task GetResultsAsync(string datasetName, string indexBy, string timeframe); + + /// + /// Get the definition of your cached dataset. + /// + /// Name of cached dataset for which to retrieve the + /// definition. + /// An DatasetDefinition containing metadata about a cached dataset. + Task GetDefinitionAsync(string datasetName); + + /// + /// Lists the first n cached dataset definitions in your project. + /// + /// How many cached dataset definitions to return at a time (1-100). + /// Defaults to 10. + /// A cursor for use in pagination. afterName is the Cached Dataset + /// name that defines your place in the list. + Task ListDefinitionsAsync(int limit = 10, + string afterName = null); + + /// + /// Lists all the dataset definitions in the project. + /// + /// An enumerable of DatasetDefinitions. + Task> ListAllDefinitionsAsync(); + + /// + /// Delete a Cached Dataset + /// + /// The name of the dataset to be deleted. + Task DeleteDatasetAsync(string datasetName); + + /// + /// Creates a new Cached Dataset + /// + /// An instance of DatasetDefinition. At minimum, it must have + /// DatasetName, DisplayName, IndexBy and Query populated. + /// An instance of DatasetDefinition populated with more information about the + /// create Dataset. + Task CreateDatasetAsync(DatasetDefinition dataset); + } +} diff --git a/Keen/Dataset/QueryDefinitionExtensions.cs b/Keen/Dataset/QueryDefinitionExtensions.cs new file mode 100644 index 0000000..616d966 --- /dev/null +++ b/Keen/Dataset/QueryDefinitionExtensions.cs @@ -0,0 +1,31 @@ +using Keen.Core.Query; + + +namespace Keen.Core.Dataset +{ + internal static class QueryDefinitionExtensions + { + public static void ValidateForCachedDataset(this QueryDefinition query) + { + if (string.IsNullOrWhiteSpace(query.AnalysisType)) + { + throw new KeenException("QueryDefinition must have an analysis type"); + } + + if (string.IsNullOrWhiteSpace(query.EventCollection)) + { + throw new KeenException("QueryDefinition must specify an event collection"); + } + + if (string.IsNullOrWhiteSpace(query.Timeframe)) + { + throw new KeenException("QueryDefinition must specify a timeframe"); + } + + if (string.IsNullOrWhiteSpace(query.Interval)) + { + throw new KeenException("QueryDefinition must specify an interval"); + } + } + } +} diff --git a/Keen/HttpClientCache.cs b/Keen/HttpClientCache.cs index 3dd943c..8fc7c72 100644 --- a/Keen/HttpClientCache.cs +++ b/Keen/HttpClientCache.cs @@ -31,7 +31,7 @@ static HttpClientCache() { } private readonly object _cacheLock; - // NOTE : We should use ConcurrentDictionary> here. if/when we upgrade the PCL + // NOTE : We should use ConcurrentDictionary> here if/when we upgrade the PCL // profile to something >= .NET 4.0. // NOTE : Use WeakReference in 4.5+ diff --git a/Keen/IKeenHttpClient.cs b/Keen/IKeenHttpClient.cs index 88d3d83..812e07c 100644 --- a/Keen/IKeenHttpClient.cs +++ b/Keen/IKeenHttpClient.cs @@ -70,5 +70,27 @@ public interface IKeenHttpClient /// The key to use for authenticating this request. /// The response message. Task DeleteAsync(Uri resource, string authKey); + + /// + /// Create and send a PUT request with the given content to the given relative resource + /// using the given key for authentication. + /// + /// The relative resource to PUT. Must be properly formatted as a + /// relative Uri. + /// The key to use for authenticating this request. + /// The PUT body to send. + /// >The response message. + Task PutAsync(string resource, string authKey, string content); + + /// + /// Create and send a PUT request with the given content to the given relative resource + /// using the given key for authentication. + /// + /// The relative resource to PUT. Must be properly formatted as a + /// relative Uri. + /// The key to use for authenticating this request. + /// The PUT body to send. + /// >The response message. + Task PutAsync(Uri resource, string authKey, string content); } } diff --git a/Keen/Keen.csproj b/Keen/Keen.csproj index 7d77c11..5744708 100644 --- a/Keen/Keen.csproj +++ b/Keen/Keen.csproj @@ -52,7 +52,12 @@ Properties\SharedVersionInfo.cs + + + + + @@ -65,6 +70,7 @@ + diff --git a/Keen/KeenClient.cs b/Keen/KeenClient.cs index e736d83..b932e92 100644 --- a/Keen/KeenClient.cs +++ b/Keen/KeenClient.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Keen.Core.Dataset; namespace Keen.Core @@ -48,6 +49,8 @@ public class KeenClient /// public IQueries Queries { get; set; } + public IDataset Datasets { get; set; } + /// /// Add a static global property. This property will be added to /// every event. @@ -116,6 +119,7 @@ private KeenClient(IProjectSettings prjSettings, EventCollection = new EventCollection(_prjSettings, keenHttpClientProvider); Event = new Event(_prjSettings, keenHttpClientProvider); Queries = new Queries(_prjSettings, keenHttpClientProvider); + Datasets = new Datasets(_prjSettings, keenHttpClientProvider); } /// @@ -970,5 +974,177 @@ public IEnumerable + /// Get query results from a Cached Dataset. + /// + /// Name of cached dataset to query. + /// The string property value you want to retrieve results by. + /// Limits retrieval of results to a specific portion of the Cached Dataset + /// An instance of Newtonsoft.Json.Linq.JObject containing query results and metadata defining the cached dataset. + public async Task QueryDatasetAsync(string datasetName, string indexBy, string timeframe) + { + return await Datasets.GetResultsAsync(datasetName, indexBy, timeframe); + } + + /// + /// Get query results from a Cached Dataset. + /// + /// Name of cached dataset to query. + /// The string property value you want to retrieve results by. + /// Limits retrieval of results to a specific portion of the Cached Dataset + /// An instance of Newtonsoft.Json.Linq.JObject containing query results and metadata defining the cached dataset. + public JObject QueryDataset(string datasetName, string indexBy, string timeframe) + { + try + { + return QueryDatasetAsync(datasetName, indexBy, timeframe).Result; + } + catch (AggregateException ex) + { + throw ex.TryUnwrap(); + } + } + + /// + /// Get the definition of your cached dataset. + /// + /// Name of cached dataset to get the definition of. + /// An instance of Keen.Core.Dataset.DatasetDefinition containing metadata about your cached dataset. + public async Task GetDatasetDefinitionAsync(string datasetName) + { + return await Datasets.GetDefinitionAsync(datasetName); + } + + /// + /// Get the definition of your cached dataset. + /// + /// Name of cached dataset to get the definition of. + /// An instance of Keen.Core.Dataset.DatasetDefinition containing metadata about your cached dataset. + public DatasetDefinition GetDatasetDefinition(string datasetName) + { + try + { + return GetDatasetDefinitionAsync(datasetName).Result; + } + catch (AggregateException ex) + { + throw ex.TryUnwrap(); + } + } + + /// + /// Lists the first n cached dataset definitions in your project. + /// + /// How many cached dataset definitions to return at a time (1-100). Defaults to 10. + /// A cursor for use in pagination. afterName is the Cached Dataset name that defines your place in the list. + /// For instance, if you make a list request and receive 100 Cached Dataset definitions, + /// ending with dataset_foo you can use dataset_foo as your afterName to retrieve the next page of definitions. + /// Lists also return with helper “NextPageUrl” that uses AfterName, + /// so your subsequent call can fetch the next page of the list easily. + /// An instance of Keen.Core.Dataset.DatasetDefinitionCollection containing the total count, next page url and list of DatasetDefinitions. + public Task ListDatasetDefinitionsAsync(int limit = 10, string afterName = null) + { + return Datasets.ListDefinitionsAsync(limit, afterName); + } + + /// + /// Lists the first n cached dataset definitions in your project. + /// + /// How many cached dataset definitions to return at a time (1-100). Defaults to 10. + /// A cursor for use in pagination. afterName is the Cached Dataset name that defines your place in the list. + /// For instance, if you make a list request and receive 100 Cached Dataset definitions, + /// ending with dataset_foo you can use dataset_foo as your afterName to retrieve the next page of definitions. + /// Lists also return with helper “NextPageUrl” that uses AfterName, + /// so your subsequent call can fetch the next page of the list easily. + /// An instance of Keen.Core.Dataset.DatasetDefinitionCollection containing the total count, next page url and list of DatasetDefinitions. + public DatasetDefinitionCollection ListDatasetDefinitions(int limit = 10, string afterName = null) + { + try + { + return ListDatasetDefinitionsAsync(limit, afterName).Result; + } + catch(AggregateException ex) + { + throw ex.TryUnwrap(); + } + } + + /// + /// Lists all the dataset definitions in your project. + /// + /// A list of Keen.Core.Dataset.DatasetDefinition + public Task> ListAllDatasetDefinitionsAsync() + { + return Datasets.ListAllDefinitionsAsync(); + } + + /// + /// Lists all the dataset definitions in your project. + /// + /// A list of Keen.Core.Dataset.DatasetDefinition + public IEnumerable ListAllDatasetDefinitions() + { + try + { + return ListAllDatasetDefinitionsAsync().Result; + } + catch (AggregateException ex) + { + throw ex.TryUnwrap(); + } + } + + /// + /// Creates a new Cached Dataset + /// + /// An instance of Keen.Core.Dataset.DatasetDefinition. It must have DatasetName, DisplayName, IndexBy and Query populated. + /// An instance of Keen.Core.Dataset.DatasetDefinition populated more information. + public Task CreateDatasetAsync(DatasetDefinition dataset) + { + return Datasets.CreateDatasetAsync(dataset); + } + + /// + /// Creates a new Cached Dataset + /// + /// An instance of Keen.Core.Dataset.DatasetDefinition. It must have DatasetName, DisplayName, IndexBy and Query populated. + /// An instance of Keen.Core.Dataset.DatasetDefinition populated more information. + public DatasetDefinition CreateDataset(DatasetDefinition dataset) + { + try + { + return CreateDatasetAsync(dataset).Result; + } + catch (AggregateException ex) + { + throw ex.TryUnwrap(); + } + } + + /// + /// Delete a Cached Dataset + /// + /// The name of the dataset to be deleted. + public Task DeleteDatasetAsync(string datasetName) + { + return Datasets.DeleteDatasetAsync(datasetName); + } + + /// + /// Delete a Cached Dataset + /// + /// The name of the dataset to be deleted. + public void DeleteDataset(string datasetName) + { + try + { + DeleteDatasetAsync(datasetName).Wait(); + } + catch (AggregateException ex) + { + throw ex.TryUnwrap(); + } + } } -} \ No newline at end of file +} diff --git a/Keen/KeenConstants.cs b/Keen/KeenConstants.cs index 4fe2c18..0b1c6a4 100644 --- a/Keen/KeenConstants.cs +++ b/Keen/KeenConstants.cs @@ -81,6 +81,9 @@ public class KeenConstants private const int bulkBatchSize = 1000; public static int BulkBatchSize { get { return bulkBatchSize; } protected set { ;} } + private const string datasetsResource = "datasets"; + public static string DatasetsResource { get { return datasetsResource; } protected set { ;} } + private const string keenProjectId = "KEEN_PROJECT_ID"; public static string KeenProjectId { get { return keenProjectId; } protected set {; } } diff --git a/Keen/KeenHttpClient.cs b/Keen/KeenHttpClient.cs index 7a6eedf..4ebccbf 100644 --- a/Keen/KeenHttpClient.cs +++ b/Keen/KeenHttpClient.cs @@ -43,7 +43,7 @@ internal static string GetRelativeUrl(string projectId, string resource) /// The relative resource to GET. Must be properly formatted as a /// relative Uri. /// The key to use for authenticating this request. - /// >The response message. + /// The response message. public Task GetAsync(string resource, string authKey) { var url = new Uri(resource, UriKind.Relative); @@ -57,7 +57,7 @@ public Task GetAsync(string resource, string authKey) /// /// The relative resource to GET. /// The key to use for authenticating this request. - /// >The response message. + /// The response message. public Task GetAsync(Uri resource, string authKey) { KeenHttpClient.RequireAuthKey(authKey); @@ -78,7 +78,7 @@ public Task GetAsync(Uri resource, string authKey) /// relative Uri. /// The key to use for authenticating this request. /// The POST body to send. - /// >The response message. + /// The response message. public Task PostAsync(string resource, string authKey, string content) { var url = new Uri(resource, UriKind.Relative); @@ -93,44 +93,12 @@ public Task PostAsync(string resource, string authKey, stri /// The relative resource to POST. /// The key to use for authenticating this request. /// The POST body to send. - /// >The response message. - public async Task PostAsync(Uri resource, - string authKey, - string content) + /// The response message. + public Task PostAsync(Uri resource, + string authKey, + string content) { - KeenHttpClient.RequireAuthKey(authKey); - - if (string.IsNullOrWhiteSpace(content)) - { - // Technically, we can encode an empty string or whitespace, but why? For now - // we use GET for querying. If we ever need to POST with no content, we should - // reorganize the logic below to never create/set the content stream. - throw new ArgumentNullException(nameof(content), "Unexpected empty content."); - } - - // If we switch PCL profiles, instead use MediaTypeFormatters (or ObjectContent)?, - // like here?: https://msdn.microsoft.com/en-us/library/system.net.http.httpclientextensions.putasjsonasync(v=vs.118).aspx - using (var contentStream = - new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(content)))) - { - // TODO : Amake sure this is the same as Add("content-type", "application/json") - contentStream.Headers.ContentType = - new MediaTypeHeaderValue(KeenHttpClient.JSON_CONTENT_TYPE); - - HttpRequestMessage post = CreateRequest(HttpMethod.Post, resource, authKey); - post.Content = contentStream; - - return await _httpClient.SendAsync(post).ConfigureAwait(false); - - // TODO : Should we do the KeenUtil.CheckApiErrorCode() here? - // TODO : Should we check the if (!responseMsg.IsSuccessStatusCode) here too? - // TODO : If we centralize error checking in this class we could have variations - // of these helpers that return string or JToken or JArray or JObject. It might - // also be nice for those options to optionally hand back the raw - // HttpResponseMessage in an out param if desired? - // TODO : Use CallerMemberNameAttribute to print error messages? - // http://stackoverflow.com/questions/3095696/how-do-i-get-the-calling-method-name-and-type-using-reflection?noredirect=1&lq=1 - } + return DispatchWithContentAsync(HttpMethod.Post, resource, authKey, content); } /// @@ -164,6 +132,78 @@ public Task DeleteAsync(Uri resource, string authKey) return _httpClient.SendAsync(delete); } + /// + /// Create and send a PUT request with the given content to the given relative resource + /// using the given key for authentication. + /// + /// The relative resource to PUT. Must be properly formatted as a + /// relative Uri. + /// The key to use for authenticating this request. + /// The PUT body to send. + /// The response message. + public Task PutAsync(string resource, string authKey, string content) + { + var url = new Uri(resource, UriKind.Relative); + + return PutAsync(url, authKey, content); + } + + /// + /// Create and send a PUT request with the given content to the given relative resource + /// using the given key for authentication. + /// + /// The relative resource to PUT. Must be properly formatted as a + /// relative Uri. + /// The key to use for authenticating this request. + /// The PUT body to send. + /// The response message. + public Task PutAsync(Uri resource, + string authKey, + string content) + { + return DispatchWithContentAsync(HttpMethod.Put, resource, authKey, content); + } + + private async Task DispatchWithContentAsync(HttpMethod httpMethod, + Uri resource, + string authKey, + string content) + { + KeenHttpClient.RequireAuthKey(authKey); + + if (string.IsNullOrWhiteSpace(content)) + { + // Technically, we can encode an empty string or whitespace, but why? For now + // we use GET for querying. If we ever need to POST with no content, we should + // reorganize the logic below to never create/set the content stream. + throw new ArgumentNullException(nameof(content), "Unexpected empty content."); + } + + // If we switch PCL profiles, instead use MediaTypeFormatters (or ObjectContent)?, + // like here?: https://msdn.microsoft.com/en-us/library/system.net.http.httpclientextensions.putasjsonasync(v=vs.118).aspx + using (var contentStream = + new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(content)))) + { + // TODO : Make sure this is the same as Add("content-type", "application/json") + contentStream.Headers.ContentType = + new MediaTypeHeaderValue(KeenHttpClient.JSON_CONTENT_TYPE); + + HttpRequestMessage request = CreateRequest(httpMethod, resource, authKey); + request.Content = contentStream; + + return await _httpClient.SendAsync(request).ConfigureAwait(false); + + // TODO : Should we do the KeenUtil.CheckApiErrorCode() here? + // TODO : Should we check the if (!responseMsg.IsSuccessStatusCode) here too? + // TODO : If we centralize error checking in this class we could have variations + // of these helpers that return string or JToken or JArray or JObject. It might + // also be nice for those options to optionally hand back the raw + // HttpResponseMessage in an out param if desired? + // TODO : Use CallerMemberNameAttribute to print error messages? + // http://stackoverflow.com/questions/3095696/how-do-i-get-the-calling-method-name-and-type-using-reflection?noredirect=1&lq=1 + } + } + private static HttpRequestMessage CreateRequest(HttpMethod verb, Uri resource, string authKey) diff --git a/Keen/Query/QueryDefinition.cs b/Keen/Query/QueryDefinition.cs new file mode 100644 index 0000000..49edecb --- /dev/null +++ b/Keen/Query/QueryDefinition.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; + + +namespace Keen.Core.Query +{ + /// + /// Holds information describing the query that is cached within a cached dataset. + /// + public class QueryDefinition + { + /// + /// Unique id of the project to analyze. + /// + public string ProjectId { get; set; } + + /// + /// The type of analysis for this query (e.g. count, count_unique, sum etc.) + /// + public string AnalysisType { get; set; } + + /// + /// Specifies the name of the event collection to analyze. + /// + public string EventCollection { get; set; } + + /// + /// Refines the scope of events to be included in the analysis based on event property + /// values. + /// + public IEnumerable Filters { get; set; } + + /// + /// Limits analysis to a specific period of time when the events occurred. + /// + public string Timeframe { get; set; } + + /// + /// Assigns a timezone offset to relative timeframes. + /// + public string Timezone { get; set; } + + /// + /// Specifies the size of time interval by which to group results. Using this parameter + /// changes the response format. + /// + public string Interval { get; set; } + + /// + /// Specifies the names of properties by which to group results. Using this parameter + /// changes the response format. + /// + public IEnumerable GroupBy { get; set; } + } +}