Skip to content

Commit 7e277b6

Browse files
Merge branch 'dev' into users/mtherien/leglink-948-change-logging
2 parents 55aa57a + d2ec3c1 commit 7e277b6

94 files changed

Lines changed: 16117 additions & 1433 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.docker/create-dbs.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'link-tenant') CREATE DA
77
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'link-validation') CREATE DATABASE [link-validation];
88
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'link-querydispatch') CREATE DATABASE [link-querydispatch];
99
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'link-account') CREATE DATABASE [link-account];
10-
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'link-submission') CREATE DATABASE [link-submission];
10+
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'link-submission') CREATE DATABASE [link-submission];
11+
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'link-mock-dmrp') CREATE DATABASE [link-mock-dmrp];

Azure_Pipelines/_deploy_all_services.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ jobs:
168168
repoName: validation
169169
containerName: validation
170170
healthUrl: https://$(env)-validation.nhsnlink.org/health
171+
# Stand-in for the CDC DMRP API, deployed to the lower environments only. Health
172+
# answers even when the mock is disabled by configuration, so this check passes for a
173+
# deliberately dormant deployment rather than reporting it as an outage.
174+
mock-dmrp:
175+
serviceName: mock-dmrp
176+
repoName: mock-dmrp
177+
containerName: mock-dmrp
178+
healthUrl: https://$(env)-mock-dmrp.nhsnlink.org/health
171179

172180
steps:
173181
- task: KubeloginInstaller@0
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
resources:
2+
pipelines:
3+
- pipeline: pipeline-trigger
4+
source: Build_And_Push_All
5+
trigger: true
6+
7+
trigger:
8+
branches:
9+
include:
10+
- dev
11+
- release/*
12+
- linkathon/*
13+
paths:
14+
include:
15+
- DotNet/MockDmrpApi/*
16+
- DotNet/Shared/*
17+
exclude:
18+
- '*'
19+
20+
pr: none
21+
22+
pool:
23+
vmImage: 'ubuntu-latest'
24+
25+
variables:
26+
- group: link-cloud-variables
27+
- name: project
28+
value: 'DotNet/MockDmrpApi/MockDmrpApi.csproj'
29+
# ServiceTests holds this service's unit tests alongside every other service's. The filter
30+
# excludes the integration suite, which needs Docker and a database the agent does not have.
31+
- name: testProject
32+
value: 'DotNet/ServiceTests/ServiceTests.csproj'
33+
- name: testFilter
34+
value: 'FullyQualifiedName~MockDmrpApi&Category!=IntegrationTests'
35+
- name: registry-repo-Name
36+
value: 'link-mock-dmrp'
37+
- name: dockerPath
38+
value: '**/DotNet/MockDmrpApi/Dockerfile'
39+
- name: serviceName
40+
value: 'MockDmrpApi'
41+
- name: projectDir
42+
value: 'DotNet/MockDmrpApi'
43+
44+
steps:
45+
- task: DotNetCoreCLI@2
46+
displayName: Restore
47+
inputs:
48+
command: restore
49+
projects: '$(project)'
50+
51+
- task: Bash@3
52+
inputs:
53+
targetType: 'inline'
54+
script: |
55+
GIT_COMMIT=$(git rev-parse --short HEAD)
56+
echo "GIT_COMMIT: ${GIT_COMMIT}"
57+
echo "##vso[task.setvariable variable=GIT_COMMIT]${GIT_COMMIT}"
58+
59+
- task: PythonScript@0
60+
displayName: "Inject Service Info"
61+
inputs:
62+
scriptSource: 'filePath'
63+
scriptPath: '$(Build.SourcesDirectory)/Scripts/set_service_info.py'
64+
workingDirectory: '$(Build.SourcesDirectory)'
65+
arguments: './ "$(projectDir)" "$(GIT_COMMIT)" "$(Build.BuildNumber)"'
66+
67+
- task: PowerShell@2
68+
inputs:
69+
targetType: 'inline'
70+
script: |
71+
if ("$(Build.SourceBranch)" -like "*release/*") {
72+
$myTag1 = "release-$(Build.SourceBranchName)-$(GIT_COMMIT)"
73+
}
74+
else {
75+
$myTag1 = "$(Build.SourceBranchName)-$(GIT_COMMIT)"
76+
}
77+
Write-Host "##vso[task.setvariable variable=MyTag]$myTag1"
78+
Write-Host "Set MyTag to: $myTag1"
79+
80+
# Regenerates the API contract from Contracts/dmrp-openapi.yaml before compiling, so a
81+
# build here fails if the spec and the implementation have diverged.
82+
- task: DotNetCoreCLI@2
83+
displayName: Build
84+
inputs:
85+
command: build
86+
projects: '$(project)'
87+
88+
- task: DotNetCoreCLI@2
89+
inputs:
90+
command: 'test'
91+
projects: '$(testProject)'
92+
arguments: '--filter "$(testFilter)"'
93+
displayName: 'Run Tests'
94+
95+
# No `condition: always()` here, unlike most of the sibling CD pipelines. The step above
96+
# runs the test suite, and always() would publish `latest` from a commit whose tests had
97+
# just failed -- the tag every environment pulls by default. Omitting the condition leaves
98+
# the default succeeded(), so a red build stops here.
99+
- task: Docker@2
100+
displayName: "Build & Push Mock DMRP API Docker Image"
101+
inputs:
102+
containerRegistry: $(containerRegistry) # Variable Group
103+
repository: $(registry-repo-name)
104+
command: 'buildAndPush'
105+
Dockerfile: $(dockerPath)
106+
tags: |
107+
latest
108+
$(MyTag)
109+
buildContext: '$(Build.Repository.LocalPath)'
110+
111+
- task: PublishPipelineArtifact@1
112+
inputs:
113+
targetPath: '$(Build.ArtifactStagingDirectory)'
114+
artifact: 'manifest'

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.10" />
7474
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="10.0.10" />
7575
<PackageVersion Include="Microsoft.Extensions.Telemetry" Version="10.1.0" />
76+
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.10.0" />
7677
<PackageVersion Include="Microsoft.Identity.Client" Version="4.84.1" />
7778
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.4.1" />
7879
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
@@ -82,6 +83,7 @@
8283
<PackageVersion Include="Moq" Version="4.20.72" />
8384
<PackageVersion Include="Moq.AutoMock" Version="4.0.3-ci0882" />
8485
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
86+
<PackageVersion Include="NSwag.MSBuild" Version="14.6.1" />
8587
<PackageVersion Include="nunit" Version="4.6.1" />
8688
<PackageVersion Include="NUnit3TestAdapter" Version="4.6.0" />
8789
<PackageVersion Include="OpenTelemetry" Version="1.15.3" />

DotNet/DMRP/Business/Managers/MeasureMappingManager.cs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
using LantanaGroup.Link.Shared.Application.Models;
33
using LantanaGroup.Link.Shared.Application.Services.Security;
44
using LantanaGroup.Link.Shared.Domain.Repositories.Interfaces;
5+
using Microsoft.Data.SqlClient;
6+
using Microsoft.Data.Sqlite;
7+
using Microsoft.EntityFrameworkCore;
58
using OpenTelemetry.Trace;
69
using System.Diagnostics;
710

@@ -12,6 +15,15 @@ public interface IMeasureMappingManager
1215
Task<MeasureMapping> CreateAsync(MeasureMapping newMeasureMapping, CancellationToken cancellationToken = default);
1316
Task UpdateAsync(string id, MeasureMapping measureMapping, CancellationToken cancellationToken = default);
1417
Task DeleteAsync(string id, CancellationToken cancellationToken = default);
18+
Task DeleteAllAsync(CancellationToken cancellationToken = default);
19+
}
20+
21+
internal sealed class DuplicateMeasureMappingException : ApplicationException
22+
{
23+
public DuplicateMeasureMappingException(Exception innerException)
24+
: base("A measure mapping with this measure and DQM already exists.", innerException)
25+
{
26+
}
1527
}
1628

1729
public class MeasureMappingManager : IMeasureMappingManager
@@ -38,16 +50,26 @@ public async Task<MeasureMapping> CreateAsync(MeasureMapping newMeasureMapping,
3850
{
3951
throw;
4052
}
53+
catch (DbUpdateException ex) when (IsUniqueIndexViolation(ex))
54+
{
55+
Activity.Current?.SetStatus(ActivityStatusCode.Error);
56+
Activity.Current?.AddException(ex);
57+
throw new DuplicateMeasureMappingException(ex);
58+
}
4159
catch (Exception ex)
4260
{
4361
Activity.Current?.SetStatus(ActivityStatusCode.Error);
4462
Activity.Current?.AddException(ex);
45-
throw new ApplicationException("Measure mapping failed to create. " + ex.Message);
63+
throw new ApplicationException("Measure mapping failed to create. " + ex.Message, ex);
4664
}
4765

4866
return newMeasureMapping;
4967
}
5068

69+
private static bool IsUniqueIndexViolation(DbUpdateException exception) =>
70+
exception.InnerException is SqliteException { SqliteExtendedErrorCode: 2067 }
71+
|| exception.InnerException is SqlException { Number: 2601 or 2627 };
72+
5173
public async Task UpdateAsync(string id, MeasureMapping measureMapping, CancellationToken cancellationToken = default)
5274
{
5375
using Activity? activity = ServiceActivitySource.Instance.StartActivity("Update Measure Mapping");
@@ -59,7 +81,9 @@ public async Task UpdateAsync(string id, MeasureMapping measureMapping, Cancella
5981
throw new ApplicationException($"Measure mapping with Id: {id} not found");
6082
}
6183

62-
// TODO: Map `measureMapping` to `existing`
84+
existing.Measure = measureMapping.Measure;
85+
existing.DQM = measureMapping.DQM;
86+
existing.Frequency = measureMapping.Frequency;
6387

6488
try
6589
{
@@ -70,6 +94,12 @@ public async Task UpdateAsync(string id, MeasureMapping measureMapping, Cancella
7094
{
7195
throw;
7296
}
97+
catch (DbUpdateException ex) when (IsUniqueIndexViolation(ex))
98+
{
99+
Activity.Current?.SetStatus(ActivityStatusCode.Error);
100+
Activity.Current?.AddException(ex);
101+
throw new DuplicateMeasureMappingException(ex);
102+
}
73103
catch (Exception ex)
74104
{
75105
Activity.Current?.SetStatus(ActivityStatusCode.Error);
@@ -105,5 +135,30 @@ public async Task DeleteAsync(string id, CancellationToken cancellationToken = d
105135
throw new ApplicationException($"Measure mapping {id} failed to delete. " + ex.Message);
106136
}
107137
}
138+
139+
public async Task DeleteAllAsync(CancellationToken cancellationToken = default)
140+
{
141+
using Activity? activity = ServiceActivitySource.Instance.StartActivity("Delete All Measure Mappings");
142+
143+
try
144+
{
145+
var measureMappings = await _repository.GetAllAsync(cancellationToken);
146+
foreach (var measureMapping in measureMappings)
147+
{
148+
_repository.Remove(measureMapping);
149+
}
150+
await _repository.SaveChangesAsync(cancellationToken);
151+
}
152+
catch (OperationCanceledException)
153+
{
154+
throw;
155+
}
156+
catch (Exception ex)
157+
{
158+
Activity.Current?.SetStatus(ActivityStatusCode.Error);
159+
Activity.Current?.AddException(ex);
160+
throw new ApplicationException("Failed to delete all measure mappings. " + ex.Message);
161+
}
162+
}
108163
}
109164
}

DotNet/DMRP/Business/Queries/MeasureMappingQueries.cs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ public interface IMeasureMappingQueries
1010
{
1111
Task<MeasureMappingModel?> GetAsync(string id, CancellationToken cancellationToken = default);
1212

13-
Task<PagedMeasureMappingDto> PagedSearchAsync(string sortBy = "Id", SortOrder sortOrder = SortOrder.Descending,
14-
int pageSize = 10, int pageNumber = 1, CancellationToken cancellationToken = default);
13+
Task<PagedMeasureMappingDto> PagedSearchAsync(SearchMeasureMappingDto searchDto, CancellationToken cancellationToken = default);
1514
}
1615

1716
public class MeasureMappingQueries : IMeasureMappingQueries
@@ -30,12 +29,18 @@ public MeasureMappingQueries(IEntityRepository<MeasureMapping> repository)
3029
return entity == null ? null : ToModel(entity);
3130
}
3231

33-
public async Task<PagedMeasureMappingDto> PagedSearchAsync(string sortBy = "Id",
34-
SortOrder sortOrder = SortOrder.Descending, int pageSize = 10, int pageNumber = 1,
35-
CancellationToken cancellationToken = default)
32+
public async Task<PagedMeasureMappingDto> PagedSearchAsync(SearchMeasureMappingDto searchDto, CancellationToken cancellationToken = default)
3633
{
37-
var (records, metadata) = await _repository.SearchAsync(m => true, sortBy, sortOrder,
38-
pageSize, pageNumber, cancellationToken);
34+
var measure = string.IsNullOrWhiteSpace(searchDto.Measure) ? null : searchDto.Measure;
35+
var dqm = string.IsNullOrWhiteSpace(searchDto.DQM) ? null : searchDto.DQM;
36+
var frequency = searchDto.Frequency;
37+
38+
var (records, metadata) = await _repository.SearchAsync(
39+
m => (measure == null || m.Measure == measure)
40+
&& (dqm == null || m.DQM == dqm)
41+
&& (!frequency.HasValue || m.Frequency == frequency.Value),
42+
searchDto.SortBy, searchDto.SortOrder,
43+
searchDto.PageSize, searchDto.PageNumber, cancellationToken);
3944

4045
return new PagedMeasureMappingDto
4146
{
@@ -44,6 +49,12 @@ public async Task<PagedMeasureMappingDto> PagedSearchAsync(string sortBy = "Id",
4449
};
4550
}
4651

47-
private static MeasureMappingModel ToModel(MeasureMapping entity) => new() { Id = entity.Id };
52+
private static MeasureMappingModel ToModel(MeasureMapping entity) => new()
53+
{
54+
Id = entity.Id,
55+
Measure = entity.Measure,
56+
DQM = entity.DQM,
57+
Frequency = entity.Frequency
58+
};
4859
}
4960
}

0 commit comments

Comments
 (0)