Skip to content

Commit 6363958

Browse files
committed
Switch getLetters endpoint to using queue table
1 parent 5f338f4 commit 6363958

19 files changed

Lines changed: 328 additions & 460 deletions

File tree

infrastructure/terraform/components/api/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ No requirements.
4545
| Name | Source | Version |
4646
|------|--------|---------|
4747
| <a name="module_amendment_event_transformer"></a> [amendment\_event\_transformer](#module\_amendment\_event\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
48-
| <a name="module_amendments_queue"></a> [amendments\_queue](#module\_amendments\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a |
48+
| <a name="module_amendments_queue"></a> [amendments\_queue](#module\_amendments\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.5/terraform-sqs.zip | n/a |
4949
| <a name="module_authorizer_lambda"></a> [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
5050
| <a name="module_ddb_alarms_letter_queue"></a> [ddb\_alarms\_letter\_queue](#module\_ddb\_alarms\_letter\_queue) | ../../modules/alarms-ddb | n/a |
5151
| <a name="module_ddb_alarms_letters"></a> [ddb\_alarms\_letters](#module\_ddb\_alarms\_letters) | ../../modules/alarms-ddb | n/a |
@@ -60,16 +60,16 @@ No requirements.
6060
| <a name="module_get_status"></a> [get\_status](#module\_get\_status) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
6161
| <a name="module_kms"></a> [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-kms.zip | n/a |
6262
| <a name="module_lambda_alarms"></a> [lambda\_alarms](#module\_lambda\_alarms) | ../../modules/alarms-lambda | n/a |
63-
| <a name="module_letter_status_updates_queue"></a> [letter\_status\_updates\_queue](#module\_letter\_status\_updates\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a |
63+
| <a name="module_letter_status_updates_queue"></a> [letter\_status\_updates\_queue](#module\_letter\_status\_updates\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.5/terraform-sqs.zip | n/a |
6464
| <a name="module_letter_updates_transformer"></a> [letter\_updates\_transformer](#module\_letter\_updates\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
6565
| <a name="module_mi_updates_transformer"></a> [mi\_updates\_transformer](#module\_mi\_updates\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip | n/a |
6666
| <a name="module_patch_letter"></a> [patch\_letter](#module\_patch\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
6767
| <a name="module_post_letters"></a> [post\_letters](#module\_post\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
6868
| <a name="module_post_mi"></a> [post\_mi](#module\_post\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
6969
| <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.26/terraform-s3bucket.zip | n/a |
7070
| <a name="module_sqs_alarms"></a> [sqs\_alarms](#module\_sqs\_alarms) | ../../modules/alarms-sqs | n/a |
71-
| <a name="module_sqs_letter_updates"></a> [sqs\_letter\_updates](#module\_sqs\_letter\_updates) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a |
72-
| <a name="module_sqs_supplier_allocator"></a> [sqs\_supplier\_allocator](#module\_sqs\_supplier\_allocator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a |
71+
| <a name="module_sqs_letter_updates"></a> [sqs\_letter\_updates](#module\_sqs\_letter\_updates) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.5/terraform-sqs.zip | n/a |
72+
| <a name="module_sqs_supplier_allocator"></a> [sqs\_supplier\_allocator](#module\_sqs\_supplier\_allocator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.5/terraform-sqs.zip | n/a |
7373
| <a name="module_supplier_allocator"></a> [supplier\_allocator](#module\_supplier\_allocator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
7474
| <a name="module_supplier_ssl"></a> [supplier\_ssl](#module\_supplier\_ssl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-ssl.zip | n/a |
7575
| <a name="module_update_letter_queue"></a> [update\_letter\_queue](#module\_update\_letter\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |

infrastructure/terraform/components/api/ddb_table_letter_queue.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ resource "aws_dynamodb_table" "letter_queue" {
1111
}
1212

1313
local_secondary_index {
14-
name = "queueTimestamp-index"
14+
name = "queueSortOrder-index"
1515
range_key = "queueTimestamp"
1616
projection_type = "ALL"
1717
}

infrastructure/terraform/components/api/locals.tf

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,18 @@ locals {
2020
destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"
2121

2222
common_lambda_env_vars = {
23-
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
24-
MI_TABLE_NAME = aws_dynamodb_table.mi.name,
25-
LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours
26-
MI_TTL_HOURS = 2160 # 90 days * 24 hours
27-
SUPPLIER_ID_HEADER = "nhsd-supplier-id",
28-
APIM_CORRELATION_HEADER = "nhsd-correlation-id",
29-
DOWNLOAD_URL_TTL_SECONDS = 60
30-
SNS_TOPIC_ARN = "${module.eventsub.sns_topic.arn}",
31-
EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters"
23+
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
24+
LETTER_QUEUE_TABLE_NAME = aws_dynamodb_table.letter_queue.name,
25+
LETTER_QUEUE_VISIBILITY_TIMEOUT = 600, # 10 minutes * 60 seconds
26+
MI_TABLE_NAME = aws_dynamodb_table.mi.name,
27+
LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours
28+
LETTER_QUEUE_TTL_HOURS = 168 # 7 days * 24 hours
29+
MI_TTL_HOURS = 2160 # 90 days * 24 hours
30+
SUPPLIER_ID_HEADER = "nhsd-supplier-id",
31+
APIM_CORRELATION_HEADER = "nhsd-correlation-id",
32+
DOWNLOAD_URL_TTL_SECONDS = 60
33+
SNS_TOPIC_ARN = "${module.eventsub.sns_topic.arn}",
34+
EVENT_SOURCE = "/data-plane/supplier-api/${var.group}/${var.environment}/letters"
3235
}
3336

3437
core_pdf_bucket_arn = "arn:aws:s3:::comms-${var.core_account_id}-eu-west-2-${var.core_environment}-api-stg-pdf-pipeline"

infrastructure/terraform/components/api/module_lambda_get_letters.tf

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,12 @@ data "aws_iam_policy_document" "get_letters_lambda" {
6363
"dynamodb:GetItem",
6464
"dynamodb:Query",
6565
"dynamodb:Scan",
66+
"dynamodb:UpdateItem",
6667
]
6768

6869
resources = [
69-
aws_dynamodb_table.letters.arn,
70-
"${aws_dynamodb_table.letters.arn}/index/supplierStatus-index"
70+
aws_dynamodb_table.letter_queue.arn,
71+
"${aws_dynamodb_table.letter_queue.arn}/index/queueSortOrder-index"
7172
]
7273
}
7374
}

internal/datastore/src/__test__/db.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ const createLetterQueueTableCommand = new CreateTableCommand({
129129
],
130130
LocalSecondaryIndexes: [
131131
{
132-
IndexName: "timestamp-index",
132+
IndexName: "queueSortOrder-index",
133133
KeySchema: [
134134
{ AttributeName: "supplierId", KeyType: "HASH" }, // Partition key for LSI
135135
{ AttributeName: "queueTimestamp", KeyType: "RANGE" }, // Sort key for LSI

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

Lines changed: 106 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import {
77
setupDynamoDBContainer,
88
} from "./db";
99
import LetterQueueRepository from "../letter-queue-repository";
10-
import { InsertPendingLetter } from "../types";
10+
import { PendingLetterBase } from "../types";
1111
import { LetterAlreadyExistsError } from "../letter-already-exists-error";
1212
import { createTestLogger } from "./logs";
1313
import { LetterDoesNotExistError } from "../letter-does-not-exist-error";
1414

15-
function createLetter(letterId = "letter1"): InsertPendingLetter {
15+
function createLetter(letterId = "letter1"): PendingLetterBase {
1616
return {
1717
letterId,
1818
supplierId: "supplier1",
@@ -47,38 +47,22 @@ describe("LetterQueueRepository", () => {
4747
afterEach(async () => {
4848
await deleteTables(db);
4949
jest.useRealTimers();
50+
jest.restoreAllMocks();
5051
});
5152

5253
afterAll(async () => {
5354
await db.container.stop();
5455
});
5556

56-
function assertTtl(ttl: number, before: number, after: number) {
57-
const expectedLower = Math.floor(
58-
before / 1000 + 60 * 60 * db.config.letterQueueTtlHours,
59-
);
60-
const expectedUpper = Math.floor(
61-
after / 1000 + 60 * 60 * db.config.lettersTtlHours,
62-
);
63-
expect(ttl).toBeGreaterThanOrEqual(expectedLower);
64-
expect(ttl).toBeLessThanOrEqual(expectedUpper);
65-
}
66-
6757
describe("putLetter", () => {
6858
it("adds a letter to the database", async () => {
69-
const before = Date.now();
59+
jest.useFakeTimers().setSystemTime(new Date("2026-03-04T13:15:45.000Z"));
7060

7161
const pendingLetter =
7262
await letterQueueRepository.putLetter(createLetter());
7363

74-
const after = Date.now();
75-
76-
const timestampInMillis = new Date(
77-
pendingLetter.queueTimestamp,
78-
).valueOf();
79-
expect(timestampInMillis).toBeGreaterThanOrEqual(before);
80-
expect(timestampInMillis).toBeLessThanOrEqual(after);
81-
assertTtl(pendingLetter.ttl, before, after);
64+
expect(pendingLetter.queueTimestamp).toBe("2026-03-04T13:15:45.000Z");
65+
expect(pendingLetter.ttl).toBe(1_772_633_745);
8266
expect(await letterExists(db, "supplier1", "letter1")).toBe(true);
8367
});
8468

@@ -134,18 +118,112 @@ describe("LetterQueueRepository", () => {
134118
).rejects.toThrow("Cannot do operations on a non-existent table");
135119
});
136120
});
121+
122+
describe("getLetters", () => {
123+
it("filters by supplierId", async () => {
124+
await letterQueueRepository.putLetter(createLetter());
125+
126+
const letters = await letterQueueRepository.getLetters("supplier2", 1);
127+
128+
expect(letters).toHaveLength(0);
129+
});
130+
131+
it("filters by queueTimestamp", async () => {
132+
jest.useFakeTimers().setSystemTime(new Date("2026-03-04T13:25:45.000Z"));
133+
await letterQueueRepository.putLetter(createLetter());
134+
jest.useFakeTimers().setSystemTime(new Date("2026-03-04T13:23:45.000Z"));
135+
136+
const letters = await letterQueueRepository.getLetters("supplier1", 1);
137+
138+
expect(letters).toHaveLength(0);
139+
});
140+
141+
it("returns letters in timestamp order", async () => {
142+
await letterQueueRepository.putLetter(createLetter("first-letter"));
143+
await letterQueueRepository.putLetter(createLetter("second-letter"));
144+
await letterQueueRepository.putLetter(createLetter("third-letter"));
145+
await letterQueueRepository.putLetter(createLetter("fourth-letter"));
146+
await letterQueueRepository.putLetter(createLetter("fifth-letter"));
147+
148+
const letters = await letterQueueRepository.getLetters("supplier1", 5);
149+
150+
expect(letters[0].letterId).toBe("first-letter");
151+
expect(letters[1].letterId).toBe("second-letter");
152+
expect(letters[2].letterId).toBe("third-letter");
153+
expect(letters[3].letterId).toBe("fourth-letter");
154+
expect(letters[4].letterId).toBe("fifth-letter");
155+
});
156+
157+
it("limits results to the supplied number", async () => {
158+
await letterQueueRepository.putLetter(createLetter("first-letter"));
159+
await letterQueueRepository.putLetter(createLetter("second-letter"));
160+
await letterQueueRepository.putLetter(createLetter("third-letter"));
161+
await letterQueueRepository.putLetter(createLetter("fourth-letter"));
162+
163+
const letters = await letterQueueRepository.getLetters("supplier1", 3);
164+
165+
expect(letters).toHaveLength(3);
166+
expect(letters[2].letterId).toBe("third-letter");
167+
});
168+
});
169+
170+
describe("updateLetterTimestamp", () => {
171+
it("updates the queueTimestamp on an existing letter", async () => {
172+
const pendingLetter =
173+
await letterQueueRepository.putLetter(createLetter());
174+
175+
await letterQueueRepository.updateLetterTimestamp(
176+
pendingLetter,
177+
new Date("2026-03-04T13:15:45.000Z"),
178+
);
179+
180+
const letter = await getLetter(db, "supplier1", "letter1");
181+
expect(letter?.queueTimestamp).toBe("2026-03-04T13:15:45.000Z");
182+
});
183+
184+
it("does nothing when the letter does not exist", async () => {
185+
await letterQueueRepository.updateLetterTimestamp(
186+
createLetter(),
187+
new Date(),
188+
);
189+
190+
expect(await letterExists(db, "supplier1", "letter1")).toBe(false);
191+
});
192+
193+
it("rethrows errors from DynamoDB when updating the letter", async () => {
194+
const misconfiguredRepository = new LetterQueueRepository(
195+
db.docClient,
196+
logger,
197+
{
198+
...db.config,
199+
letterQueueTableName: "nonexistent-table",
200+
},
201+
);
202+
await expect(
203+
misconfiguredRepository.updateLetterTimestamp(
204+
createLetter(),
205+
new Date(),
206+
),
207+
).rejects.toThrow("Cannot do operations on a non-existent table");
208+
});
209+
});
137210
});
138211

139-
async function letterExists(
140-
db: DBContext,
141-
supplierId: string,
142-
letterId: string,
143-
): Promise<boolean> {
212+
async function getLetter(db: DBContext, supplierId: string, letterId: string) {
144213
const result = await db.docClient.send(
145214
new GetCommand({
146215
TableName: db.config.letterQueueTableName,
147216
Key: { supplierId, letterId },
148217
}),
149218
);
150-
return result.Item !== undefined;
219+
return result.Item;
220+
}
221+
222+
async function letterExists(
223+
db: DBContext,
224+
supplierId: string,
225+
letterId: string,
226+
): Promise<boolean> {
227+
const letter = await getLetter(db, supplierId, letterId);
228+
return letter !== undefined;
151229
}

0 commit comments

Comments
 (0)