diff --git a/src/Api/Controllers/EventsController.cs b/src/Api/Controllers/EventsController.cs index fbd99d95b9..42264cbc79 100644 --- a/src/Api/Controllers/EventsController.cs +++ b/src/Api/Controllers/EventsController.cs @@ -8,6 +8,7 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Api; using Bit.Core.Services; using Bit.Core; +using Bit.Core.Models.Data; namespace Bit.Api.Controllers { @@ -31,18 +32,19 @@ namespace Bit.Api.Controllers [HttpGet("")] public async Task> GetUser( - [FromQuery]DateTime? start = null, [FromQuery]DateTime? end = null) + [FromQuery]DateTime? start = null, [FromQuery]DateTime? end = null, [FromQuery]string continuationToken = null) { var dateRange = GetDateRange(start, end); var userId = _userService.GetProperUserId(User).Value; - var events = await _eventRepository.GetManyByUserAsync(userId, dateRange.Item1, dateRange.Item2); - var responses = events.Select(e => new EventResponseModel(e)); - return new ListResponseModel(responses); + var result = await _eventRepository.GetManyByUserAsync(userId, dateRange.Item1, dateRange.Item2, + new PageOptions { ContinuationToken = continuationToken }); + var responses = result.Data.Select(e => new EventResponseModel(e)); + return new ListResponseModel(responses, result.ContinuationToken); } [HttpGet("~/organizations/{id}/events")] public async Task> GetOrganization(string id, - [FromQuery]DateTime? start = null, [FromQuery]DateTime? end = null) + [FromQuery]DateTime? start = null, [FromQuery]DateTime? end = null, [FromQuery]string continuationToken = null) { var orgId = new Guid(id); if(!_currentContext.OrganizationAdmin(orgId)) @@ -51,9 +53,10 @@ namespace Bit.Api.Controllers } var dateRange = GetDateRange(start, end); - var events = await _eventRepository.GetManyByOrganizationAsync(orgId, dateRange.Item1, dateRange.Item2); - var responses = events.Select(e => new EventResponseModel(e)); - return new ListResponseModel(responses); + var result = await _eventRepository.GetManyByOrganizationAsync(orgId, dateRange.Item1, dateRange.Item2, + new PageOptions { ContinuationToken = continuationToken }); + var responses = result.Data.Select(e => new EventResponseModel(e)); + return new ListResponseModel(responses, result.ContinuationToken); } private Tuple GetDateRange(DateTime? start, DateTime? end) diff --git a/src/Core/Models/Api/Response/ListResponseModel.cs b/src/Core/Models/Api/Response/ListResponseModel.cs index 092dd7230b..96553b1439 100644 --- a/src/Core/Models/Api/Response/ListResponseModel.cs +++ b/src/Core/Models/Api/Response/ListResponseModel.cs @@ -5,12 +5,14 @@ namespace Bit.Core.Models.Api { public class ListResponseModel : ResponseModel where T : ResponseModel { - public ListResponseModel(IEnumerable data) + public ListResponseModel(IEnumerable data, string continuationToken = null) : base("list") { Data = data; + ContinuationToken = continuationToken; } public IEnumerable Data { get; set; } + public string ContinuationToken { get; set; } } } diff --git a/src/Core/Models/Data/PageOptions.cs b/src/Core/Models/Data/PageOptions.cs new file mode 100644 index 0000000000..d01ab656f1 --- /dev/null +++ b/src/Core/Models/Data/PageOptions.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Data +{ + public class PageOptions + { + public string ContinuationToken { get; set; } + public int PageSize { get; set; } = 100; + } +} diff --git a/src/Core/Models/Data/PagedResult.cs b/src/Core/Models/Data/PagedResult.cs new file mode 100644 index 0000000000..d8765ee6e2 --- /dev/null +++ b/src/Core/Models/Data/PagedResult.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Bit.Core.Models.Data +{ + public class PagedResult + { + public List Data { get; set; } = new List(); + public string ContinuationToken { get; set; } + } +} diff --git a/src/Core/Repositories/IEventRepository.cs b/src/Core/Repositories/IEventRepository.cs index 01058b6c84..9d5c2bc1be 100644 --- a/src/Core/Repositories/IEventRepository.cs +++ b/src/Core/Repositories/IEventRepository.cs @@ -7,8 +7,10 @@ namespace Bit.Core.Repositories { public interface IEventRepository { - Task> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate); - Task> GetManyByOrganizationAsync(Guid organizationId, DateTime startDate, DateTime endDate); + Task> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate, + PageOptions pageOptions); + Task> GetManyByOrganizationAsync(Guid organizationId, DateTime startDate, DateTime endDate, + PageOptions pageOptions); Task CreateAsync(IEvent e); Task CreateManyAsync(IList e); } diff --git a/src/Core/Repositories/SqlServer/EventRepository.cs b/src/Core/Repositories/SqlServer/EventRepository.cs index bbc40bc524..5a0fa80657 100644 --- a/src/Core/Repositories/SqlServer/EventRepository.cs +++ b/src/Core/Repositories/SqlServer/EventRepository.cs @@ -19,13 +19,15 @@ namespace Bit.Core.Repositories.SqlServer : base(connectionString) { } - public Task> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate) + public Task> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate, + PageOptions pageOptions) { // TODO throw new NotImplementedException(); } - public Task> GetManyByOrganizationAsync(Guid organizationId, DateTime startDate, DateTime endDate) + public Task> GetManyByOrganizationAsync(Guid organizationId, + DateTime startDate, DateTime endDate, PageOptions pageOptions) { throw new NotImplementedException(); } diff --git a/src/Core/Repositories/TableStorage/EventRepository.cs b/src/Core/Repositories/TableStorage/EventRepository.cs index 547cc22dd6..ed0447cd28 100644 --- a/src/Core/Repositories/TableStorage/EventRepository.cs +++ b/src/Core/Repositories/TableStorage/EventRepository.cs @@ -24,63 +24,40 @@ namespace Bit.Core.Repositories.TableStorage _table = tableClient.GetTableReference("event"); } - public async Task> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate) + public async Task> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate, + PageOptions pageOptions) { var start = CoreHelpers.DateTimeToTableStorageKey(startDate); var end = CoreHelpers.DateTimeToTableStorageKey(endDate); + var filter = MakeFilter($"UserId={userId}", $"Date={start}", $"Date={end}"); - var rowFilter = TableQuery.CombineFilters( - TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, $"Date={start}_"), - TableOperators.And, - TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThanOrEqual, $"Date={end}`")); + var query = new TableQuery().Where(filter).Take(pageOptions.PageSize); + var result = new PagedResult(); + var continuationToken = DeserializeContinuationToken(pageOptions?.ContinuationToken); - var filter = TableQuery.CombineFilters( - TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, $"UserId={userId}"), - TableOperators.And, - rowFilter); + var queryResults = await _table.ExecuteQuerySegmentedAsync(query, continuationToken); + result.ContinuationToken = SerializeContinuationToken(queryResults.ContinuationToken); + result.Data.AddRange(queryResults.Results); - var query = new TableQuery().Where(filter); - var results = new List(); - TableContinuationToken continuationToken = null; - do - { - var queryResults = await _table.ExecuteQuerySegmentedAsync(query, continuationToken); - continuationToken = queryResults.ContinuationToken; - results.AddRange(queryResults.Results); - } - while(continuationToken != null); - - return results.Select(r => r as IEvent).ToList(); + return result; } - public async Task> GetManyByOrganizationAsync(Guid organizationId, - DateTime startDate, DateTime endDate) + public async Task> GetManyByOrganizationAsync(Guid organizationId, + DateTime startDate, DateTime endDate, PageOptions pageOptions) { var start = CoreHelpers.DateTimeToTableStorageKey(startDate); var end = CoreHelpers.DateTimeToTableStorageKey(endDate); + var filter = MakeFilter($"OrganizationId={organizationId}", $"Date={start}", $"Date={end}"); - var rowFilter = TableQuery.CombineFilters( - TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, $"Date={start}_"), - TableOperators.And, - TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThanOrEqual, $"Date={end}`")); + var query = new TableQuery().Where(filter).Take(pageOptions.PageSize); + var result = new PagedResult(); + var continuationToken = DeserializeContinuationToken(pageOptions?.ContinuationToken); - var filter = TableQuery.CombineFilters( - TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, $"OrganizationId={organizationId}"), - TableOperators.And, - rowFilter); + var queryResults = await _table.ExecuteQuerySegmentedAsync(query, continuationToken); + result.ContinuationToken = SerializeContinuationToken(queryResults.ContinuationToken); + result.Data.AddRange(queryResults.Results); - var query = new TableQuery().Where(filter); - var results = new List(); - TableContinuationToken continuationToken = null; - do - { - var queryResults = await _table.ExecuteQuerySegmentedAsync(query, continuationToken); - continuationToken = queryResults.ContinuationToken; - results.AddRange(queryResults.Results); - } - while(continuationToken != null); - - return results.Select(r => r as IEvent).ToList(); + return result; } public async Task CreateAsync(IEvent e) @@ -142,5 +119,51 @@ namespace Bit.Core.Repositories.TableStorage { await _table.ExecuteAsync(TableOperation.Insert(entity)); } + + private string MakeFilter(string partitionKey, string rowStart, string rowEnd) + { + var rowFilter = TableQuery.CombineFilters( + TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.LessThanOrEqual, $"{rowStart}`"), + TableOperators.And, + TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, $"{rowEnd}_")); + + return TableQuery.CombineFilters( + TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey), + TableOperators.And, + rowFilter); + } + + private string SerializeContinuationToken(TableContinuationToken token) + { + if(token == null) + { + return null; + } + + return string.Format("{0}__{1}__{2}__{3}", (int)token.TargetLocation, token.NextTableName, + token.NextPartitionKey, token.NextRowKey); + } + + private TableContinuationToken DeserializeContinuationToken(string token) + { + if(string.IsNullOrWhiteSpace(token)) + { + return null; + } + + var tokenParts = token.Split(new string[] { "__" }, StringSplitOptions.None); + if(tokenParts.Length < 4 || !Enum.TryParse(tokenParts[0], out StorageLocation tLoc)) + { + return null; + } + + return new TableContinuationToken + { + TargetLocation = tLoc, + NextTableName = string.IsNullOrWhiteSpace(tokenParts[1]) ? null : tokenParts[1], + NextPartitionKey = string.IsNullOrWhiteSpace(tokenParts[2]) ? null : tokenParts[2], + NextRowKey = string.IsNullOrWhiteSpace(tokenParts[3]) ? null : tokenParts[3] + }; + } } } diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 325187629d..356210038b 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -11,6 +11,7 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; using Dapper; +using System.Globalization; namespace Bit.Core.Utilities { @@ -18,6 +19,7 @@ namespace Bit.Core.Utilities { private static readonly long _baseDateTicks = new DateTime(1900, 1, 1).Ticks; private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private static readonly DateTime _max = new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static readonly Random _random = new Random(); private static string _version; private static readonly string _qwertyDvorakMap = "-=qwertyuiop[]asdfghjkl;'zxcvbnm,./_+QWERTYUIO" + @@ -412,12 +414,16 @@ namespace Bit.Core.Utilities public static string DateTimeToTableStorageKey(DateTime? date = null) { - if(date == null) + if(date.HasValue) + { + date = date.Value.ToUniversalTime(); + } + else { date = DateTime.UtcNow; } - return date.Value.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss.fff"); + return _max.Subtract(date.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); } } }