Add Summary Count for History Endpoints (#3661)
* Added count for all history * Added necessary extra tests * Added E2E Test * Addressed PR comments * Fixed E2E Tests * fix test build issue * Fixed test name * Updated tests * Fixed test issue * Fix test date/time format
This commit is contained in:
Родитель
c2ea2a56d4
Коммит
4ae7bec855
|
@ -23,6 +23,9 @@ public class HistoryModel
|
|||
[FromQuery(Name = KnownQueryParameterNames.Count)]
|
||||
public int? Count { get; set; }
|
||||
|
||||
[FromQuery(Name = KnownQueryParameterNames.Summary)]
|
||||
public string Summary { get; set; }
|
||||
|
||||
[FromQuery(Name = KnownQueryParameterNames.ContinuationToken)]
|
||||
public string ContinuationToken { get; set; }
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Search
|
|||
PartialDateTime since,
|
||||
PartialDateTime before,
|
||||
int? count,
|
||||
string summary,
|
||||
string continuationToken,
|
||||
string sort,
|
||||
CancellationToken cancellationToken,
|
||||
|
|
|
@ -10,6 +10,7 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using EnsureThat;
|
||||
using Hl7.Fhir.Rest;
|
||||
using Microsoft.Health.Core;
|
||||
using Microsoft.Health.Fhir.Core.Exceptions;
|
||||
using Microsoft.Health.Fhir.Core.Extensions;
|
||||
|
@ -77,6 +78,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Search
|
|||
PartialDateTime since,
|
||||
PartialDateTime before,
|
||||
int? count,
|
||||
string summary,
|
||||
string continuationToken,
|
||||
string sort,
|
||||
CancellationToken cancellationToken,
|
||||
|
@ -157,6 +159,10 @@ namespace Microsoft.Health.Fhir.Core.Features.Search
|
|||
{
|
||||
queryParameters.Add(Tuple.Create(KnownQueryParameterNames.Count, count.ToString()));
|
||||
}
|
||||
else if ((count.HasValue && count == 0) || (summary is not null && summary.Equals(SummaryType.Count.ToString(), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
queryParameters.Add(Tuple.Create(KnownQueryParameterNames.Summary, SummaryType.Count.ToString()));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sort))
|
||||
{
|
||||
|
|
|
@ -15,20 +15,21 @@ namespace Microsoft.Health.Fhir.Core.Messages.Search
|
|||
{
|
||||
private readonly string _capability;
|
||||
|
||||
public SearchResourceHistoryRequest(PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string continuationToken = null, string sort = null)
|
||||
public SearchResourceHistoryRequest(PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string summary = null, string continuationToken = null, string sort = null)
|
||||
{
|
||||
Since = since;
|
||||
Before = before;
|
||||
At = at;
|
||||
Count = count;
|
||||
Summary = summary;
|
||||
ContinuationToken = continuationToken;
|
||||
Sort = sort;
|
||||
|
||||
_capability = "CapabilityStatement.rest.interaction.where(code = 'history-system').exists()";
|
||||
}
|
||||
|
||||
public SearchResourceHistoryRequest(string resourceType, PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string continuationToken = null, string sort = null)
|
||||
: this(since, before, at, count, continuationToken, sort)
|
||||
public SearchResourceHistoryRequest(string resourceType, PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string summary = null, string continuationToken = null, string sort = null)
|
||||
: this(since, before, at, count, summary, continuationToken, sort)
|
||||
{
|
||||
EnsureArg.IsNotNullOrWhiteSpace(resourceType, nameof(resourceType));
|
||||
|
||||
|
@ -37,8 +38,8 @@ namespace Microsoft.Health.Fhir.Core.Messages.Search
|
|||
_capability = $"CapabilityStatement.rest.resource.where(type = '{resourceType}').interaction.where(code = 'history-type').exists()";
|
||||
}
|
||||
|
||||
public SearchResourceHistoryRequest(string resourceType, string resourceId, PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string continuationToken = null, string sort = null)
|
||||
: this(resourceType, since, before, at, count, continuationToken, sort)
|
||||
public SearchResourceHistoryRequest(string resourceType, string resourceId, PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string summary = null, string continuationToken = null, string sort = null)
|
||||
: this(resourceType, since, before, at, count, summary, continuationToken, sort)
|
||||
{
|
||||
EnsureArg.IsNotNullOrWhiteSpace(resourceType, nameof(resourceType));
|
||||
EnsureArg.IsNotNullOrWhiteSpace(resourceId, nameof(resourceId));
|
||||
|
@ -60,6 +61,8 @@ namespace Microsoft.Health.Fhir.Core.Messages.Search
|
|||
|
||||
public int? Count { get; }
|
||||
|
||||
public string Summary { get; }
|
||||
|
||||
public string ContinuationToken { get; }
|
||||
|
||||
public string Sort { get; }
|
||||
|
|
|
@ -295,6 +295,7 @@ namespace Microsoft.Health.Fhir.Api.Controllers
|
|||
historyModel.Before,
|
||||
historyModel.At,
|
||||
historyModel.Count,
|
||||
historyModel.Summary,
|
||||
historyModel.ContinuationToken,
|
||||
historyModel.Sort,
|
||||
HttpContext.RequestAborted);
|
||||
|
@ -320,6 +321,7 @@ namespace Microsoft.Health.Fhir.Api.Controllers
|
|||
historyModel.Before,
|
||||
historyModel.At,
|
||||
historyModel.Count,
|
||||
historyModel.Summary,
|
||||
historyModel.ContinuationToken,
|
||||
historyModel.Sort,
|
||||
HttpContext.RequestAborted);
|
||||
|
@ -348,6 +350,7 @@ namespace Microsoft.Health.Fhir.Api.Controllers
|
|||
historyModel.Before,
|
||||
historyModel.At,
|
||||
historyModel.Count,
|
||||
historyModel.Summary,
|
||||
historyModel.ContinuationToken,
|
||||
historyModel.Sort,
|
||||
HttpContext.RequestAborted);
|
||||
|
|
|
@ -80,6 +80,7 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Operations.Everything
|
|||
Arg.Any<int?>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<string>(),
|
||||
CancellationToken.None,
|
||||
Arg.Any<bool>()).Returns(searchResult);
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search
|
|||
|
||||
var searchResult = new SearchResult(Enumerable.Empty<SearchResultEntry>(), null, null, new Tuple<string, string>[0]);
|
||||
|
||||
_searchService.SearchHistoryAsync(request.ResourceType, null, null, null, null, null, null, null, CancellationToken.None).Returns(searchResult);
|
||||
_searchService.SearchHistoryAsync(request.ResourceType, null, null, null, null, null, null, null, null, CancellationToken.None).Returns(searchResult);
|
||||
|
||||
var expectedBundle = new Bundle().ToResourceElement();
|
||||
|
||||
|
|
|
@ -5,13 +5,16 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Hl7.Fhir.Model;
|
||||
using Hl7.Fhir.Rest;
|
||||
using Hl7.Fhir.Serialization;
|
||||
using Microsoft.Health.Fhir.Core.Exceptions;
|
||||
using Microsoft.Health.Fhir.Core.Extensions;
|
||||
using Microsoft.Health.Fhir.Core.Features;
|
||||
using Microsoft.Health.Fhir.Core.Features.Persistence;
|
||||
using Microsoft.Health.Fhir.Core.Features.Search;
|
||||
using Microsoft.Health.Fhir.Core.Models;
|
||||
|
@ -104,7 +107,7 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search
|
|||
|
||||
_searchService.SearchImplementation = options => SearchResult.Empty(_unsupportedQueryParameters);
|
||||
|
||||
await Assert.ThrowsAsync<ResourceNotFoundException>(() => _searchService.SearchHistoryAsync(resourceType, resourceId, null, null, null, null, null, null, CancellationToken.None));
|
||||
await Assert.ThrowsAsync<ResourceNotFoundException>(() => _searchService.SearchHistoryAsync(resourceType, resourceId, null, null, null, null, null, null, null, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -121,11 +124,49 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search
|
|||
|
||||
_fhirDataStore.GetAsync(Arg.Any<ResourceKey>(), Arg.Any<CancellationToken>()).Returns(resourceWrapper);
|
||||
|
||||
SearchResult searchResult = await _searchService.SearchHistoryAsync(resourceType, resourceId, PartialDateTime.Parse("2018"), null, null, null, null, null, CancellationToken.None);
|
||||
SearchResult searchResult = await _searchService.SearchHistoryAsync(resourceType, resourceId, PartialDateTime.Parse("2018"), null, null, null, null, null, null, CancellationToken.None);
|
||||
|
||||
Assert.Empty(searchResult.Results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GivenAHistorySearch_WhenUsingSummaryCountOrCountZero_ThenSearchOptionsShouldBeProperlyFormed()
|
||||
{
|
||||
var expectedSearchOptions = new SearchOptions();
|
||||
|
||||
_searchOptionsFactory.Create(
|
||||
Arg.Any<string>(),
|
||||
Arg.Is<IReadOnlyList<Tuple<string, string>>>(list => list.Any(item => item.Item1 == KnownQueryParameterNames.Summary && item.Item2 == SummaryType.Count.ToString())),
|
||||
resourceVersionTypes: ResourceVersionType.Latest | ResourceVersionType.History | ResourceVersionType.SoftDeleted).Returns(expectedSearchOptions);
|
||||
|
||||
var expectedSearchResult = SearchResult.Empty(_unsupportedQueryParameters);
|
||||
|
||||
_searchService.SearchImplementation = options =>
|
||||
{
|
||||
Assert.Same(expectedSearchOptions, options);
|
||||
|
||||
return expectedSearchResult;
|
||||
};
|
||||
|
||||
var observation = new Observation { Id = "123" }.ToResourceElement();
|
||||
_fhirDataStore.GetAsync(
|
||||
Arg.Is<ResourceKey>(key => key.ResourceType == observation.InstanceType && key.Id == observation.Id),
|
||||
Arg.Any<CancellationToken>()).Returns(new ResourceWrapper(observation, _rawResourceFactory.Create(observation, keepMeta: true), _resourceRequest, false, null, null, null));
|
||||
|
||||
SearchResult allCountZero = await _searchService.SearchHistoryAsync(null, null, null, null, null, 0, null, null, null, CancellationToken.None);
|
||||
SearchResult allSummaryCount = await _searchService.SearchHistoryAsync(null, null, null, null, null, null, "count", null, null, CancellationToken.None);
|
||||
SearchResult allPatientCountZero = await _searchService.SearchHistoryAsync(observation.InstanceType, null, null, null, null, 0, null, null, null, CancellationToken.None);
|
||||
SearchResult allPatientSummaryCount = await _searchService.SearchHistoryAsync(observation.InstanceType, null, null, null, null, null, "count", null, null, CancellationToken.None);
|
||||
SearchResult singlePatientCountZero = await _searchService.SearchHistoryAsync(observation.InstanceType, observation.Id, null, null, null, 0, null, null, null, CancellationToken.None);
|
||||
SearchResult singlePatientSummaryCount = await _searchService.SearchHistoryAsync(observation.InstanceType, observation.Id, null, null, null, null, "count", null, null, CancellationToken.None);
|
||||
Assert.Same(expectedSearchResult, allCountZero);
|
||||
Assert.Same(expectedSearchResult, allPatientCountZero);
|
||||
Assert.Same(expectedSearchResult, allPatientCountZero);
|
||||
Assert.Same(expectedSearchResult, allPatientSummaryCount);
|
||||
Assert.Same(expectedSearchResult, singlePatientCountZero);
|
||||
Assert.Same(expectedSearchResult, singlePatientSummaryCount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
|
|
|
@ -114,29 +114,29 @@ namespace Microsoft.Health.Fhir.Core.Extensions
|
|||
return result.Bundle;
|
||||
}
|
||||
|
||||
public static async Task<ResourceElement> SearchResourceHistoryAsync(this IMediator mediator, PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string continuationToken = null, string sort = null, CancellationToken cancellationToken = default)
|
||||
public static async Task<ResourceElement> SearchResourceHistoryAsync(this IMediator mediator, PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string summary = null, string continuationToken = null, string sort = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureArg.IsNotNull(mediator, nameof(mediator));
|
||||
|
||||
var result = await mediator.Send(new SearchResourceHistoryRequest(since, before, at, count, continuationToken, sort), cancellationToken);
|
||||
var result = await mediator.Send(new SearchResourceHistoryRequest(since, before, at, count, summary, continuationToken, sort), cancellationToken);
|
||||
|
||||
return result.Bundle;
|
||||
}
|
||||
|
||||
public static async Task<ResourceElement> SearchResourceHistoryAsync(this IMediator mediator, string resourceType, PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string continuationToken = null, string sort = null, CancellationToken cancellationToken = default)
|
||||
public static async Task<ResourceElement> SearchResourceHistoryAsync(this IMediator mediator, string resourceType, PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string summary = null, string continuationToken = null, string sort = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureArg.IsNotNull(mediator, nameof(mediator));
|
||||
|
||||
var result = await mediator.Send(new SearchResourceHistoryRequest(resourceType, since, before, at, count, continuationToken, sort), cancellationToken);
|
||||
var result = await mediator.Send(new SearchResourceHistoryRequest(resourceType, since, before, at, count, summary, continuationToken, sort), cancellationToken);
|
||||
|
||||
return result.Bundle;
|
||||
}
|
||||
|
||||
public static async Task<ResourceElement> SearchResourceHistoryAsync(this IMediator mediator, string resourceType, string resourceId, PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string continuationToken = null, string sort = null, CancellationToken cancellationToken = default)
|
||||
public static async Task<ResourceElement> SearchResourceHistoryAsync(this IMediator mediator, string resourceType, string resourceId, PartialDateTime since = null, PartialDateTime before = null, PartialDateTime at = null, int? count = null, string summary = null, string continuationToken = null, string sort = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureArg.IsNotNull(mediator, nameof(mediator));
|
||||
|
||||
var result = await mediator.Send(new SearchResourceHistoryRequest(resourceType, resourceId, since, before, at, count, continuationToken, sort), cancellationToken);
|
||||
var result = await mediator.Send(new SearchResourceHistoryRequest(resourceType, resourceId, since, before, at, count, summary, continuationToken, sort), cancellationToken);
|
||||
|
||||
return result.Bundle;
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Search
|
|||
request.Since,
|
||||
request.Before,
|
||||
request.Count,
|
||||
request.Summary,
|
||||
request.ContinuationToken,
|
||||
request.Sort,
|
||||
cancellationToken);
|
||||
|
|
|
@ -93,6 +93,49 @@ namespace Microsoft.Health.Fhir.Tests.E2E.Rest
|
|||
userMessage: $"Record 0's latest update ({readResponse[0].Resource.Meta.LastUpdated}) is not greater or equal than Record's 1 latest update ({readResponse[1].Resource.Meta.LastUpdated}).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait(Traits.Priority, Priority.One)]
|
||||
public async Task GivenTestResourcesWithUpdatesAndDeletes_WhenGettingResourceHistoryCount_TheServerShouldReturnCorrectCount()
|
||||
{
|
||||
// 3 versions, 2 history 1 delete
|
||||
_createdResource.Resource.Effective = new FhirDateTime(DateTimeOffset.UtcNow);
|
||||
await _client.UpdateAsync(_createdResource.Resource);
|
||||
await _client.DeleteAsync(_createdResource.Resource);
|
||||
|
||||
// 3 base exta resources
|
||||
await _client.CreateAsync(Samples.GetDefaultPatient().ToPoco<Patient>());
|
||||
var extraResource2 = await _client.CreateAsync(Samples.GetDefaultPatient().ToPoco<Patient>());
|
||||
var extraResource3 = await _client.CreateAsync(Samples.GetDefaultObservation().ToPoco<Observation>());
|
||||
|
||||
// 3 more versions on the extras
|
||||
extraResource2.Resource.BirthDate = "2022-12-02";
|
||||
await _client.UpdateAsync(extraResource2.Resource);
|
||||
await _client.DeleteAsync(extraResource2.Resource);
|
||||
extraResource3.Resource.Effective = new FhirDateTime(DateTimeOffset.UtcNow);
|
||||
await _client.UpdateAsync(extraResource3.Resource);
|
||||
|
||||
var sinceTime = HttpUtility.UrlEncode(_createdResource.Resource.Meta.LastUpdated.Value.AddMilliseconds(-1).ToString("o"));
|
||||
|
||||
var allSummaryCountResult = await _client.SearchAsync($"/_history?_since={sinceTime}&_summary=count");
|
||||
var allSummaryCountZero = await _client.SearchAsync($"/_history?_since={sinceTime}&_count=0");
|
||||
var allObservationSummaryCountResult = await _client.SearchAsync($"/Observation/_history?_since={sinceTime}&_summary=count");
|
||||
var allObservationSummaryCountZero = await _client.SearchAsync($"/Observation/_history?_since={sinceTime}&_count=0");
|
||||
var observationSummaryCountResult = await _client.SearchAsync($"/Observation/{_createdResource.Resource.Id}/_history?_since={sinceTime}&_summary=count");
|
||||
var observationSummaryCountZero = await _client.SearchAsync($"/Observation/{_createdResource.Resource.Id}/_history?_since={sinceTime}&_count=0");
|
||||
|
||||
// 9 versions total for all resources.
|
||||
Assert.Equal(9, allSummaryCountResult.Resource.Total);
|
||||
Assert.Equal(9, allSummaryCountZero.Resource.Total);
|
||||
|
||||
// 5 versions across only observations (first one create, update, delete - second create, update).
|
||||
Assert.Equal(5, allObservationSummaryCountResult.Resource.Total);
|
||||
Assert.Equal(5, allObservationSummaryCountZero.Resource.Total);
|
||||
|
||||
// 3 versions across single observation (create, update, delete).
|
||||
Assert.Equal(3, observationSummaryCountResult.Resource.Total);
|
||||
Assert.Equal(3, observationSummaryCountZero.Resource.Total);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait(Traits.Priority, Priority.One)]
|
||||
public async Task GivenATypeAndId_WhenGettingResourceHistoryWithAlternateSort_TheServerShouldReturnTheAppropriateBundleSuccessfully()
|
||||
|
|
Загрузка…
Ссылка в новой задаче