-
Notifications
You must be signed in to change notification settings - Fork 2
feat: DTOSS-10704 - Replace NEMS Subscriptions with new Caas based updates. Added ManageCaasSubscription function #1575
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 45 commits
Commits
Show all changes
60 commits
Select commit
Hold shift + click to select a range
749e8f7
temp commit
MWClayson-NHS 19a54b9
adding mesh client code
MWClayson-NHS 471e624
mesh integration tests
MWClayson-NHS fba7ce8
feat: added stubbed version of ManageCaasSubscription function, same …
SamAinsworth-NHS 85aa980
fix: corrected placeholder config in compose.core.yml
SamAinsworth-NHS f757433
fix: unit test project references were missing
SamAinsworth-NHS 60fc83f
testing parquet file
MWClayson-NHS a3b8292
fix: cleared up forwarder code - reimplemented NemsSubscriptionDataSe…
SamAinsworth-NHS fb44429
fix: added db configuration to core.yml for managecaassubscription fu…
SamAinsworth-NHS 7be2593
feat: additional unit tests, catch around mesh logic
SamAinsworth-NHS 207e5e5
Merge branch 'feat/new-caas-subscription-function' into feat/nems-rep…
MWClayson-NHS ce16899
integation tests
MWClayson-NHS bb63bab
Mesh Polling
MWClayson-NHS 2c9febc
fix: addressed comments from previous PR
SamAinsworth-NHS a098fb7
mesh mailbox extension
MWClayson-NHS 9e6958f
mesh integrated
MWClayson-NHS 64ada3a
Merge branch 'feat/nems-replacement-mesh-branch' into Fix/manage-caas…
SamAinsworth-NHS 94f4078
feat: Subscription source addition to nems subscription data table
SamAinsworth-NHS d03fbd1
Merge branch 'feat/nems-replacement-mesh-branch' into feat/add-mesh-e…
SamAinsworth-NHS 9ab4a27
feat: insert record into nems sub table when new sub created
SamAinsworth-NHS d1b8ee4
fix: complete proper ef migration for new subscription source data ta…
SamAinsworth-NHS b1eb776
fix: removed CaasNemsSubscriptionAccessor
SamAinsworth-NHS 5c4b631
fix: renamed parquet column to nhs_number
SamAinsworth-NHS 7855354
fix: added further config for local docker testing
SamAinsworth-NHS c0d293b
fix: removed pointless comments
SamAinsworth-NHS 1178ad3
fix: undone changes to other functions in compose yml
SamAinsworth-NHS c637d84
fix: reverted change to nems-mesh-retrieval compose
SamAinsworth-NHS d7f7dfb
fix: clean up ambiguity for certificate helper and fix unit test
SamAinsworth-NHS d1311b7
feat: add mesh enum to nems data service and table (#1564)
SamAinsworth-NHS a2971cd
Merge branch 'main' into feat/nems-replacement-mesh-branch
SamAinsworth-NHS 7e9aa12
Merge branch 'main' into feat/nems-replacement-mesh-branch
SamAinsworth-NHS 105424d
fix: resolved PR comments - required attribute, removal of comments, …
SamAinsworth-NHS 4f8bd7f
fix: removed ManageNemsSubscriptionDataServiceURL from ManageCaasSubs…
SamAinsworth-NHS 6ae4fdd
Merge branch 'main' into feat/nems-replacement-mesh-branch
SamAinsworth-NHS 736a1d6
test: added further basic unit tests around the MeshPoller and shared…
SamAinsworth-NHS eb26290
fix: removed test that could not complete in pipeline
SamAinsworth-NHS fa3deba
fix: attempt to fix trx upload upon test failure to diagnose which te…
SamAinsworth-NHS 6e18c53
fix: readded test, corrected pem mock
SamAinsworth-NHS c575751
fix: missing using statement, correcting test setup
SamAinsworth-NHS 682668f
fix: added AAA comments
SamAinsworth-NHS 5ec557e
fix: added XML docs for methods re Contributing.md
SamAinsworth-NHS 8d66e12
fix: sonar issues including nullable warnings, glob syntax, unused vars
SamAinsworth-NHS a6c13b0
fix: method signatures in unit tests
SamAinsworth-NHS b32413e
Merge branch 'main' into feat/nems-replacement-mesh-branch
SamAinsworth-NHS a7d2272
Merge branch 'main' into feat/nems-replacement-mesh-branch
SamAinsworth-NHS b2a2e04
fix: removed nullable values from ManageCaasSubscriptionConfig to ali…
SamAinsworth-NHS e963312
fix: added string? nullable flag to SendSubscriptionRequest method re…
SamAinsworth-NHS 3300269
fix: added null check to message id, implemented exception handler in…
SamAinsworth-NHS 0d45c9b
fix: switched from DefaultAzureCredential to ManagedIdentityCredentia…
SamAinsworth-NHS 59cdd67
fix: dev tfvars for ManageCaasSubscription; added db and kv connectio…
SamAinsworth-NHS c891d7a
fix: updated unit tests to include exception handler
SamAinsworth-NHS a87cd99
fix: extended stub in ManageCaasSubscription to cover polling, and co…
SamAinsworth-NHS 6a17972
Merge branch 'main' into feat/nems-replacement-mesh-branch
SamAinsworth-NHS 8c09edd
fix: added required keyword to required values in ManageCaasSubscript…
SamAinsworth-NHS e481d6b
fix: log to exception handler in main catch block in Subscribe endpoi…
SamAinsworth-NHS 2f311f4
fix: covered all catch blocks with exception logs to handler within M…
SamAinsworth-NHS 18d2e66
feat: added explicit stubbed log on startup to make it clear upon dia…
SamAinsworth-NHS fca17cc
test: Added unit tests around all code flows, removed validation test…
SamAinsworth-NHS 25d81b3
fix: removed remaining 'CAAS' filenames from logs as they're not corr…
SamAinsworth-NHS b46a965
Merge branch 'main' into feat/nems-replacement-mesh-branch
SamAinsworth-NHS File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
...ication/CohortManager/src/Functions/DemographicServices/ManageCaasSubscription/Dockerfile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| FROM mcr.microsoft.com/dotnet/sdk:8.0 AS base | ||
|
|
||
| COPY ./Shared /Shared | ||
| WORKDIR /Shared | ||
|
|
||
| RUN mkdir -p /home/site/wwwroot && \ | ||
| dotnet publish ./Common/Common.csproj --output /home/site/wwwroot && \ | ||
| dotnet publish ./Model/Model.csproj --output /home/site/wwwroot && \ | ||
| dotnet publish ./Data/Data.csproj --output /home/site/wwwroot && \ | ||
| dotnet publish ./Utilities/Utilities.csproj --output /home/site/wwwroot && \ | ||
| dotnet publish ./DataServices.Client/DataServices.Client.csproj --output /home/site/wwwroot && \ | ||
| dotnet publish ./DataServices.Core/DataServices.Core.csproj --output /home/site/wwwroot && \ | ||
| dotnet publish ./DataServices.Database/DataServices.Database.csproj --output /home/site/wwwroot | ||
|
|
||
| FROM base AS function | ||
|
|
||
| COPY ./DemographicServices/ManageCaasSubscription /src/dotnet-function-app | ||
| WORKDIR /src/dotnet-function-app | ||
|
|
||
| RUN --mount=type=cache,target=/root/.nuget/packages \ | ||
| dotnet publish ./*.csproj --output /home/site/wwwroot | ||
|
|
||
| # To enable ssh & remote debugging on app service change the base image to the one below | ||
| # FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0-appservice | ||
| FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 | ||
| ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ | ||
| AzureFunctionsJobHost__Logging__Console__IsEnabled=true | ||
|
|
||
| COPY --from=function ["/home/site/wwwroot", "/home/site/wwwroot"] |
24 changes: 24 additions & 0 deletions
24
...rtManager/src/Functions/DemographicServices/ManageCaasSubscription/HealthCheckFunction.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| namespace NHS.CohortManager.DemographicServices; | ||
|
|
||
| using System.Threading.Tasks; | ||
| using HealthChecks.Extensions; | ||
| using Microsoft.Azure.Functions.Worker; | ||
| using Microsoft.Azure.Functions.Worker.Http; | ||
| using Microsoft.Extensions.Diagnostics.HealthChecks; | ||
|
|
||
| public class HealthCheckFunction | ||
| { | ||
| private readonly HealthCheckService _healthCheckService; | ||
|
|
||
| public HealthCheckFunction(HealthCheckService healthCheckService) | ||
| { | ||
| _healthCheckService = healthCheckService; | ||
| } | ||
|
|
||
| [Function("health")] | ||
| public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req) | ||
| { | ||
| return await HealthCheckServiceExtensions.CreateHealthCheckResponseAsync(req, _healthCheckService); | ||
| } | ||
| } | ||
|
|
182 changes: 182 additions & 0 deletions
182
...anager/src/Functions/DemographicServices/ManageCaasSubscription/ManageCaasSubscription.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| namespace NHS.CohortManager.DemographicServices; | ||
|
|
||
| using System.Net; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Azure.Functions.Worker; | ||
| using Microsoft.Azure.Functions.Worker.Http; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Options; | ||
| using Common; | ||
| using System.Collections.Specialized; | ||
| using System.Text; | ||
| using DataServices.Core; | ||
| using Model; | ||
| using NHS.CohortManager.DemographicServices; | ||
|
|
||
| /// <summary> | ||
| /// Azure Functions endpoints for managing CaaS subscriptions via MESH and data services. | ||
| /// </summary> | ||
| public class ManageCaasSubscription | ||
| { | ||
| private readonly ILogger<ManageCaasSubscription> _logger; | ||
| private readonly ICreateResponse _createResponse; | ||
| private readonly ManageCaasSubscriptionConfig _config; | ||
| private readonly IMeshSendCaasSubscribe _meshSendCaasSubscribe; | ||
| private readonly IRequestHandler<NemsSubscription> _requestHandler; | ||
| private readonly IDataServiceAccessor<NemsSubscription> _nemsSubscriptionAccessor; | ||
| private readonly IMeshPoller _meshPoller; | ||
|
|
||
| public ManageCaasSubscription( | ||
| ILogger<ManageCaasSubscription> logger, | ||
| ICreateResponse createResponse, | ||
| IOptions<ManageCaasSubscriptionConfig> config, | ||
| IMeshSendCaasSubscribe meshSendCaasSubscribe, | ||
| IRequestHandler<NemsSubscription> requestHandler, | ||
| IDataServiceAccessor<NemsSubscription> nemsSubscriptionAccessor, | ||
| IMeshPoller meshPoller) | ||
| { | ||
| _logger = logger; | ||
| _createResponse = createResponse; | ||
| _config = config.Value; | ||
| _meshSendCaasSubscribe = meshSendCaasSubscribe; | ||
| _requestHandler = requestHandler; | ||
| _nemsSubscriptionAccessor = nemsSubscriptionAccessor; | ||
| _meshPoller = meshPoller; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a new CaaS subscription for the given NHS number and persists a record. | ||
| /// </summary> | ||
| /// <param name="req">HTTP request containing an <c>nhsNumber</c> query parameter.</param> | ||
| /// <returns>HTTP 200 on success, 400 for invalid input, or 500 on error.</returns> | ||
| [Function("Subscribe")] | ||
| public async Task<HttpResponseData> Subscribe([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req) | ||
| { | ||
| try | ||
| { | ||
| var nhsNumber = req.Query["nhsNumber"]; | ||
| if (!ValidationHelper.ValidateNHSNumber(nhsNumber!)) | ||
| { | ||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.BadRequest, req, "NHS number is required and must be valid format."); | ||
| } | ||
|
|
||
| // Forward to MeshSendCaasSubscribeStub (Shared) | ||
| var nhsNo = long.Parse(nhsNumber!); | ||
| var toMailbox = _config.CaasToMailbox!; | ||
| var fromMailbox = _config.CaasFromMailbox!; | ||
| var messageId = await _meshSendCaasSubscribe.SendSubscriptionRequest(nhsNo, toMailbox, fromMailbox); | ||
|
|
||
| // Save a record to NEMS_SUBSCRIPTION table with source = MESH | ||
| var record = new NemsSubscription | ||
| { | ||
| SubscriptionId = messageId, | ||
| NhsNumber = nhsNo, | ||
| RecordInsertDateTime = DateTime.UtcNow, | ||
| SubscriptionSource = SubscriptionSource.MESH | ||
| }; | ||
| var saved = await _nemsSubscriptionAccessor.InsertSingle(record); | ||
| if (!saved) | ||
| { | ||
| _logger.LogError("Failed to write CAAS subscription record to database"); | ||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.InternalServerError, req, "Failed to save subscription record."); | ||
|
alex-clayton-1 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| _logger.LogInformation("CAAS Subscribe forwarded to Mesh stub. MessageId: {Msg}", messageId); | ||
|
alex-clayton-1 marked this conversation as resolved.
Outdated
|
||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.OK, req, $"Subscription request accepted. MessageId: {messageId}"); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Error sending CAAS subscribe request"); | ||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.InternalServerError, req, "An error occurred while sending the CAAS subscription request."); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Stub endpoint to remove a CaaS subscription for the given NHS number. | ||
| /// </summary> | ||
| /// <param name="req">HTTP request containing an <c>nhsNumber</c> query parameter.</param> | ||
| /// <returns>HTTP 200 for the stub, or 400 for invalid input.</returns> | ||
| [Function("Unsubscribe")] | ||
| public async Task<HttpResponseData> Unsubscribe([HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req) | ||
| { | ||
| var nhsNumber = req.Query["nhsNumber"]; | ||
| if (!ValidationHelper.ValidateNHSNumber(nhsNumber!)) | ||
| { | ||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.BadRequest, req, "NHS number is required and must be valid format."); | ||
| } | ||
|
|
||
| _logger.LogInformation("[CAAS-Stub] Unsubscribe called"); | ||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.OK, req, "Stub: CAAS subscription would be removed."); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Checks subscription status for a given NHS number. | ||
| /// </summary> | ||
| /// <param name="req">HTTP request containing an <c>nhsNumber</c> query parameter.</param> | ||
| /// <returns>HTTP 200 when an active subscription is found, 404 if not, or 400/500 on error.</returns> | ||
| [Function("CheckSubscriptionStatus")] | ||
| public async Task<HttpResponseData> CheckSubscriptionStatus([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req) | ||
| { | ||
| try | ||
| { | ||
| _logger.LogInformation("Received check subscription request"); | ||
|
|
||
| string? nhsNumber = req.Query["nhsNumber"]; | ||
|
|
||
| if (!ValidationHelper.ValidateNHSNumber(nhsNumber!)) | ||
| { | ||
| _logger.LogError("NHS number is required and must be valid format"); | ||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.BadRequest, req, "NHS number is required and must be valid format."); | ||
| } | ||
|
|
||
| var record = await _nemsSubscriptionAccessor.GetSingle(i => i.NhsNumber == long.Parse(nhsNumber!)); | ||
| string? subscriptionId = record?.SubscriptionId; | ||
|
|
||
| if (string.IsNullOrEmpty(subscriptionId)) | ||
| { | ||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.NotFound, req, "No subscription found for this NHS number."); | ||
| } | ||
|
|
||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.OK, req, $"Active subscription found. Subscription ID: {subscriptionId}"); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Error checking subscription status"); | ||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.InternalServerError, req, "An error occurred while checking subscription status."); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Pass-through data service endpoint for CRUD operations on the NEMS subscription data object. | ||
| /// </summary> | ||
| /// <param name="req">HTTP request containing payload and route parameters.</param> | ||
| /// <param name="key">Optional key or route tail for the data service.</param> | ||
| /// <returns>HTTP response from the underlying data service handler.</returns> | ||
| [Function("NemsSubscriptionDataService")] | ||
| public async Task<HttpResponseData> NemsSubscriptionDataService([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", "put", "delete", Route = "NemsSubscriptionDataService/{*key}")] HttpRequestData req, string? key) | ||
| { | ||
| try | ||
| { | ||
| _logger.LogInformation("DataService Request Received Method: {Method}, DataObject {DataType} ", req.Method, typeof(NemsSubscription)); | ||
| var result = await _requestHandler.HandleRequest(req, key); | ||
| return result; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "An error has occurred in data service"); | ||
| return await _createResponse.CreateHttpResponseWithBodyAsync(HttpStatusCode.InternalServerError, req, "An error occurred while processing the data service request."); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Nightly timer trigger to validate the configured MESH mailbox via handshake. | ||
| /// </summary> | ||
| /// <param name="myTimer">Timer trigger context.</param> | ||
| [Function("PollMeshMailbox")] | ||
| public async Task RunAsync([TimerTrigger("59 23 * * *")] TimerInfo myTimer) | ||
| { | ||
| await _meshPoller.ExecuteHandshake(_config.CaasFromMailbox!); | ||
| } | ||
|
|
||
|
|
||
| } | ||
35 changes: 35 additions & 0 deletions
35
...er/src/Functions/DemographicServices/ManageCaasSubscription/ManageCaasSubscription.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <ProjectGuid>{B0B2C4F4-AB0E-4C4D-9E1B-1F2C4E7F8A10}</ProjectGuid> | ||
| <TargetFramework>net8.0</TargetFramework> | ||
| <AzureFunctionsVersion>v4</AzureFunctionsVersion> | ||
| <OutputType>Exe</OutputType> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||
| <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.0.0" /> | ||
| <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" /> | ||
| <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.0.2" /> | ||
| <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Timer" Version="4.3.1" /> | ||
| <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.4" /> | ||
| <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.3" /> | ||
| </ItemGroup> | ||
| <ItemGroup> | ||
| <None Update="host.json"> | ||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
| </None> | ||
| <None Update="local.settings.json"> | ||
| <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> | ||
| <CopyToPublishDirectory>Never</CopyToPublishDirectory> | ||
| </None> | ||
| </ItemGroup> | ||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\Shared\Common\Common.csproj" /> | ||
| <ProjectReference Include="..\..\Shared\HealthChecks\HealthChecks.csproj" /> | ||
| <ProjectReference Include="..\..\Shared\Model\Model.csproj" /> | ||
| <ProjectReference Include="..\..\Shared\DataServices.Core\DataServices.Core.csproj" /> | ||
| <ProjectReference Include="..\..\Shared\DataServices.Database\DataServices.Database.csproj" /> | ||
| </ItemGroup> | ||
| </Project> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.