Skip to content

Commit e802a39

Browse files
authored
CCM-11189: POST MI lambda (#199)
* CCM-11201 Store MI in DDB * CCM-11189 Lambda to POST management information
1 parent 3bca7ed commit e802a39

33 files changed

+815
-72
lines changed

infrastructure/terraform/components/api/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ No requirements.
4040
| <a name="module_kms"></a> [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-kms.zip | n/a |
4141
| <a name="module_logging_bucket"></a> [logging\_bucket](#module\_logging\_bucket) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |
4242
| <a name="module_patch_letter"></a> [patch\_letter](#module\_patch\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
43+
| <a name="module_post_mi"></a> [post\_mi](#module\_post\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
4344
| <a name="module_s3bucket_test_letters"></a> [s3bucket\_test\_letters](#module\_s3bucket\_test\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |
4445
| <a name="module_supplier_ssl"></a> [supplier\_ssl](#module\_supplier\_ssl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-ssl.zip | n/a |
4546
## Outputs

infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
5050
resources = [
5151
module.authorizer_lambda.function_arn,
5252
module.get_letter.function_arn,
53+
module.get_letter_data.function_arn,
5354
module.get_letters.function_arn,
5455
module.patch_letter.function_arn,
55-
module.get_letter_data.function_arn
56+
module.post_mi.function_arn
5657
]
5758
}
5859
}

infrastructure/terraform/components/api/locals.tf

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,25 @@ locals {
55
root_domain_nameservers = local.acct.route53_zone_nameservers["supplier-api"]
66

77
openapi_spec = templatefile("${path.module}/resources/spec.tmpl.json", {
8-
APIG_EXECUTION_ROLE_ARN = aws_iam_role.api_gateway_execution_role.arn
9-
AWS_REGION = var.region
10-
AUTHORIZER_LAMBDA_ARN = module.authorizer_lambda.function_arn
11-
GET_LETTER_LAMBDA_ARN = module.get_letter.function_arn
12-
GET_LETTERS_LAMBDA_ARN = module.get_letters.function_arn
8+
APIG_EXECUTION_ROLE_ARN = aws_iam_role.api_gateway_execution_role.arn
9+
AWS_REGION = var.region
10+
AUTHORIZER_LAMBDA_ARN = module.authorizer_lambda.function_arn
11+
GET_LETTER_LAMBDA_ARN = module.get_letter.function_arn
12+
GET_LETTERS_LAMBDA_ARN = module.get_letters.function_arn
1313
GET_LETTER_DATA_LAMBDA_ARN = module.get_letter_data.function_arn
14-
PATCH_LETTER_LAMBDA_ARN = module.patch_letter.function_arn
14+
PATCH_LETTER_LAMBDA_ARN = module.patch_letter.function_arn
15+
POST_MI_LAMBDA_ARN = module.post_mi.function_arn
1516
})
1617

1718
destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"
1819

1920
common_lambda_env_vars = {
20-
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
21-
LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours
22-
SUPPLIER_ID_HEADER = "nhsd-supplier-id",
23-
APIM_CORRELATION_HEADER = "nhsd-correlation-id",
21+
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
22+
MI_TABLE_NAME = aws_dynamodb_table.mi.name,
23+
LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours
24+
MI_TTL_HOURS = 2160 # 90 days * 24 hours
25+
SUPPLIER_ID_HEADER = "nhsd-supplier-id",
26+
APIM_CORRELATION_HEADER = "nhsd-correlation-id",
2427
DOWNLOAD_URL_TTL_SECONDS = 60
2528
}
2629
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
module "post_mi" {
2+
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"
3+
4+
function_name = "post_mi"
5+
description = "Add management information"
6+
7+
aws_account_id = var.aws_account_id
8+
component = var.component
9+
environment = var.environment
10+
project = var.project
11+
region = var.region
12+
group = var.group
13+
14+
log_retention_in_days = var.log_retention_in_days
15+
kms_key_arn = module.kms.key_arn
16+
17+
iam_policy_document = {
18+
body = data.aws_iam_policy_document.post_mi_lambda.json
19+
}
20+
21+
function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
22+
function_code_base_path = local.aws_lambda_functions_dir_path
23+
function_code_dir = "api-handler/dist"
24+
function_include_common = true
25+
handler_function_name = "postMI"
26+
runtime = "nodejs22.x"
27+
memory = 128
28+
timeout = 5
29+
log_level = var.log_level
30+
31+
force_lambda_code_deploy = var.force_lambda_code_deploy
32+
enable_lambda_insights = false
33+
34+
send_to_firehose = true
35+
log_destination_arn = local.destination_arn
36+
log_subscription_role_arn = local.acct.log_subscription_role_arn
37+
38+
lambda_env_vars = merge(local.common_lambda_env_vars, {})
39+
}
40+
41+
data "aws_iam_policy_document" "post_mi_lambda" {
42+
statement {
43+
sid = "KMSPermissions"
44+
effect = "Allow"
45+
46+
actions = [
47+
"kms:Decrypt",
48+
"kms:GenerateDataKey",
49+
]
50+
51+
resources = [
52+
module.kms.key_arn, ## Requires shared kms module
53+
]
54+
}
55+
56+
statement {
57+
sid = "AllowDynamoDBAccess"
58+
effect = "Allow"
59+
60+
actions = [
61+
"dynamodb:PutItem",
62+
]
63+
64+
resources = [
65+
aws_dynamodb_table.mi.arn,
66+
]
67+
}
68+
}

infrastructure/terraform/components/api/resources/spec.tmpl.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,45 @@
202202
}
203203
}
204204
]
205+
},
206+
"/mi": {
207+
"post": {
208+
"description": "Provide management information.",
209+
"operationId": "postMI",
210+
"requestBody": {
211+
"required": true
212+
},
213+
"responses": {
214+
"201": {
215+
"description": "Resource created"
216+
},
217+
"400": {
218+
"description": "Bad request, invalid input data"
219+
},
220+
"500": {
221+
"description": "Server error"
222+
}
223+
},
224+
"security": [
225+
{
226+
"LambdaAuthorizer": []
227+
}
228+
],
229+
"x-amazon-apigateway-integration": {
230+
"contentHandling": "CONVERT_TO_TEXT",
231+
"credentials": "${APIG_EXECUTION_ROLE_ARN}",
232+
"httpMethod": "POST",
233+
"passthroughBehavior": "WHEN_NO_TEMPLATES",
234+
"responses": {
235+
".*": {
236+
"statusCode": "200"
237+
}
238+
},
239+
"timeoutInMillis": 29000,
240+
"type": "AWS_PROXY",
241+
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${POST_MI_LAMBDA_ARN}/invocations"
242+
}
243+
}
205244
}
206245
}
207246
}

internal/datastore/src/__test__/db.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export async function setupDynamoDBContainer() {
3030
region: 'us-west-2',
3131
endpoint,
3232
lettersTableName: 'letters',
33-
ttlHours: 1
33+
miTableName: 'management-info',
34+
lettersTtlHours: 1,
35+
miTtlHours: 1
3436
};
3537

3638
return {
@@ -44,10 +46,7 @@ export async function setupDynamoDBContainer() {
4446

4547
export type DBContext = Awaited<ReturnType<typeof setupDynamoDBContainer>>;
4648

47-
export async function createTables(context: DBContext) {
48-
const { ddbClient } = context;
49-
50-
await ddbClient.send(new CreateTableCommand({
49+
const createLetterTableCommand = new CreateTableCommand({
5150
TableName: 'letters',
5251
BillingMode: 'PAY_PER_REQUEST',
5352
KeySchema: [
@@ -72,15 +71,37 @@ export async function createTables(context: DBContext) {
7271
{ AttributeName: 'supplierStatus', AttributeType: 'S' },
7372
{ AttributeName: 'supplierStatusSk', AttributeType: 'S' },
7473
]
75-
}));
74+
});
7675

77-
await ddbClient.send(new UpdateTimeToLiveCommand({
76+
const updateTimeToLiveCommand = new UpdateTimeToLiveCommand({
7877
TableName: 'letters',
7978
TimeToLiveSpecification: {
8079
AttributeName: 'ttl',
8180
Enabled: true
8281
}
83-
}));
82+
});
83+
84+
const createMITableCommand = new CreateTableCommand({
85+
TableName: 'management-info',
86+
BillingMode: 'PAY_PER_REQUEST',
87+
KeySchema: [
88+
{ AttributeName: 'supplierId', KeyType: 'HASH' }, // Partition key
89+
{ AttributeName: 'id', KeyType: 'RANGE' } // Sort key
90+
],
91+
AttributeDefinitions: [
92+
{ AttributeName: 'supplierId', AttributeType: 'S' },
93+
{ AttributeName: 'id', AttributeType: 'S' },
94+
]
95+
});
96+
97+
98+
export async function createTables(context: DBContext) {
99+
const { ddbClient } = context;
100+
101+
await ddbClient.send(createLetterTableCommand);
102+
await ddbClient.send(updateTimeToLiveCommand);
103+
104+
await ddbClient.send(createMITableCommand);
84105
}
85106

86107

@@ -90,4 +111,8 @@ export async function deleteTables(context: DBContext) {
90111
await ddbClient.send(new DeleteTableCommand({
91112
TableName: 'letters'
92113
}));
114+
115+
await ddbClient.send(new DeleteTableCommand({
116+
TableName: 'management-info'
117+
}));
93118
}

internal/datastore/src/__test__/letter-repository.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ describe('LetterRepository', () => {
307307

308308
const mockSend = jest.fn().mockResolvedValue({ Items: null });
309309
const mockDdbClient = { send: mockSend } as any;
310-
const repo = new LetterRepository(mockDdbClient, { debug: jest.fn() } as any, { lettersTableName: 'letters', ttlHours: 1 });
310+
const repo = new LetterRepository(mockDdbClient, { debug: jest.fn() } as any, { lettersTableName: 'letters', lettersTtlHours: 1 });
311311

312312
const letters = await repo.getLettersBySupplier('supplier1', 'PENDING', 10);
313313
expect(letters).toEqual([]);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Logger } from "pino";
2+
import { setupDynamoDBContainer, createTables, DBContext, deleteTables } from "./db";
3+
import { createTestLogger, LogStream } from "./logs";
4+
import { MIRepository } from "../mi-repository";
5+
6+
// Database tests can take longer, especially with setup and teardown
7+
jest.setTimeout(30000);
8+
9+
10+
describe('MiRepository', () => {
11+
let db: DBContext;
12+
let miRepository: MIRepository;
13+
let logStream: LogStream;
14+
let logger: Logger;
15+
16+
17+
beforeAll(async () => {
18+
db = await setupDynamoDBContainer();
19+
});
20+
21+
beforeEach(async () => {
22+
await createTables(db);
23+
(
24+
{ logStream, logger } = createTestLogger()
25+
);
26+
27+
miRepository = new MIRepository(db.docClient, logger, db.config);
28+
});
29+
30+
afterEach(async () => {
31+
await deleteTables(db);
32+
jest.useRealTimers();
33+
});
34+
35+
afterAll(async () => {
36+
await db.container.stop();
37+
});
38+
39+
describe('putMi', () => {
40+
41+
it('creates a letter with id and timestamps', async () => {
42+
43+
jest.useFakeTimers();
44+
// Month is zero-indexed in JS Date
45+
jest.setSystemTime(new Date(2020, 1, 1));
46+
const mi = {
47+
specificationId: 'spec1',
48+
supplierId: 'supplier1',
49+
groupId:'group1',
50+
lineItem: 'item1',
51+
quantity: 12,
52+
timestamp: new Date().toISOString(),
53+
stockRemaining: 0
54+
};
55+
56+
const persistedMi = await(miRepository.putMI(mi));
57+
58+
expect(persistedMi).toEqual(expect.objectContaining({
59+
id: expect.any(String),
60+
createdAt: '2020-02-01T00:00:00.000Z',
61+
updatedAt: '2020-02-01T00:00:00.000Z',
62+
ttl: 1580518800, // 2020-02-01T00:01:00.000Z, seconds since epoch
63+
...mi
64+
}));
65+
});
66+
});
67+
});

internal/datastore/src/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@ export type DatastoreConfig = {
22
region: string,
33
endpoint?: string,
44
lettersTableName: string,
5-
ttlHours: number
5+
miTableName: string,
6+
lettersTtlHours: number,
7+
miTtlHours: number
68
}

internal/datastore/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './types';
2+
export * from './mi-repository';
23
export * from './letter-repository';
34
export * from './types';

0 commit comments

Comments
 (0)