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:
Mikael Weaver 2024-01-12 16:18:20 -08:00 коммит произвёл GitHub
Родитель c2ea2a56d4
Коммит 4ae7bec855
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 116 добавлений и 14 удалений

Просмотреть файл

@ -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()