Skip to content

Commit 116f66b

Browse files
Merge branch 'main' into feature/CCM-13610-letterRequestEventPerformance_thirdBranch
2 parents 24b2072 + 8ac9dbf commit 116f66b

11 files changed

Lines changed: 124 additions & 32 deletions

File tree

infrastructure/terraform/components/api/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ No requirements.
1717
| <a name="input_core_environment"></a> [core\_environment](#input\_core\_environment) | Environment of Core | `string` | `"prod"` | no |
1818
| <a name="input_default_tags"></a> [default\_tags](#input\_default\_tags) | A map of default tags to apply to all taggable resources within the component | `map(string)` | `{}` | no |
1919
| <a name="input_disable_gateway_execute_endpoint"></a> [disable\_gateway\_execute\_endpoint](#input\_disable\_gateway\_execute\_endpoint) | Disable the execution endpoint for the API Gateway | `bool` | `true` | no |
20-
| <a name="input_enable_backups"></a> [enable\_backups](#input\_enable\_backups) | Enable backups | `bool` | `false` | no |
2120
| <a name="input_environment"></a> [environment](#input\_environment) | The name of the tfscaffold environment | `string` | n/a | yes |
2221
| <a name="input_eventpub_control_plane_bus_arn"></a> [eventpub\_control\_plane\_bus\_arn](#input\_eventpub\_control\_plane\_bus\_arn) | ARN of the EventBridge control plane bus for eventpub | `string` | `""` | no |
2322
| <a name="input_eventpub_data_plane_bus_arn"></a> [eventpub\_data\_plane\_bus\_arn](#input\_eventpub\_data\_plane\_bus\_arn) | ARN of the EventBridge data plane bus for eventpub | `string` | `""` | no |

infrastructure/terraform/components/api/ddb_table_letters.tf

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,10 @@ resource "aws_dynamodb_table" "letters" {
4141
enabled = true
4242
}
4343

44-
tags = var.default_tags
44+
tags = merge(
45+
local.default_tags,
46+
{
47+
NHSE-Enable-Dynamo-Backup-Acct = "True"
48+
}
49+
)
4550
}

infrastructure/terraform/components/api/ddb_table_mi.tf

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,10 @@ resource "aws_dynamodb_table" "mi" {
2424
enabled = true
2525
}
2626

27-
tags = var.default_tags
27+
tags = merge(
28+
local.default_tags,
29+
{
30+
NHSE-Enable-Dynamo-Backup-Acct = "True"
31+
}
32+
)
2833
}

infrastructure/terraform/components/api/ddb_table_suppliers.tf

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,10 @@ resource "aws_dynamodb_table" "suppliers" {
3030
enabled = true
3131
}
3232

33-
tags = var.default_tags
33+
tags = merge(
34+
local.default_tags,
35+
{
36+
NHSE-Enable-Dynamo-Backup-Acct = "True"
37+
}
38+
)
3439
}

infrastructure/terraform/components/api/modules_eventsub.tf

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,5 @@ module "eventsub" {
2222
sns_success_logging_sample_percent = 0
2323

2424
event_cache_expiry_days = 30
25-
enable_event_cache = true
2625
shared_infra_account_id = var.shared_infra_account_id
2726
}

infrastructure/terraform/components/api/variables.tf

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,6 @@ variable "commit_id" {
6363
default = "HEAD"
6464
}
6565

66-
variable "enable_backups" {
67-
type = bool
68-
description = "Enable backups"
69-
default = false
70-
}
71-
7266
variable "force_destroy" {
7367
type = bool
7468
description = "Flag to force deletion of S3 buckets"

infrastructure/terraform/modules/eventsub/module_s3bucket_event_cache.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ module "s3bucket_event_cache" {
4949

5050
default_tags = {
5151
Name = "Event Cache Storage"
52+
NHSE-Enable-S3-Backup-Acct = "True"
5253
}
5354
}
5455

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ describe("LetterRepository", () => {
494494
await checkLetterStatus("supplier1", "letter59", "PENDING");
495495
});
496496

497+
// eslint-disable-next-line jest/expect-expect
497498
test("should skip array gaps", async () => {
498499
const letters = [];
499500
letters[0] = createLetter("supplier1", "letter1");
@@ -516,4 +517,71 @@ describe("LetterRepository", () => {
516517
]),
517518
).rejects.toThrow("Cannot do operations on a non-existent table");
518519
});
520+
521+
test("should paginate through multiple pages when fetching letters by supplier", async () => {
522+
const mockSend = jest
523+
.fn()
524+
// first call returns 30 items with a LastEvaluatedKey
525+
.mockResolvedValueOnce({
526+
Items: Array.from({ length: 30 }, (_, i) => ({
527+
id: `letter${(i + 1).toString().padStart(3, "0")}`,
528+
status: "PENDING",
529+
specificationId: "specification1",
530+
groupId: "group1",
531+
})),
532+
LastEvaluatedKey: { id: "letter030", supplierId: "supplier1" },
533+
})
534+
// second call returns remaining 20 items without LastEvaluatedKey
535+
.mockResolvedValueOnce({
536+
Items: Array.from({ length: 20 }, (_, i) => ({
537+
id: `letter${(i + 31).toString().padStart(3, "0")}`,
538+
status: "PENDING",
539+
specificationId: "specification1",
540+
groupId: "group1",
541+
})),
542+
LastEvaluatedKey: undefined,
543+
});
544+
545+
const mockDdbClient = { send: mockSend } as any;
546+
const repo = new LetterRepository(mockDdbClient, logger, db.config);
547+
548+
// request 50 letters - should require 2 DynamoDB queries due to mocked pagination
549+
const letters = await repo.getLettersBySupplier("supplier1", "PENDING", 50);
550+
551+
// verify all 50 letters were returned
552+
expect(letters).toHaveLength(50);
553+
554+
// verify two send calls were made (2 pages)
555+
expect(mockSend).toHaveBeenCalledTimes(2);
556+
557+
// verify the second call included the ExclusiveStartKey from first response
558+
const secondCallInput = mockSend.mock.calls[1][0].input;
559+
expect(secondCallInput.ExclusiveStartKey).toEqual({
560+
id: "letter030",
561+
supplierId: "supplier1",
562+
});
563+
});
564+
565+
test("should respect limit when fewer items available than requested", async () => {
566+
// create only 10 letters
567+
for (let i = 1; i <= 10; i++) {
568+
await letterRepository.putLetter(
569+
createLetter(
570+
"supplier1",
571+
`letter${i.toString().padStart(2, "0")}`,
572+
"PENDING",
573+
),
574+
);
575+
}
576+
577+
// request 50 letters but only 10 exist
578+
const letters = await letterRepository.getLettersBySupplier(
579+
"supplier1",
580+
"PENDING",
581+
50,
582+
);
583+
584+
expect(letters).toHaveLength(10);
585+
expect(letters.every((l) => l.status === "PENDING")).toBe(true);
586+
});
519587
});

internal/datastore/src/letter-repository.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -230,23 +230,39 @@ export class LetterRepository {
230230
status: string,
231231
limit: number,
232232
): Promise<LetterBase[]> {
233+
const items: Record<string, any>[] = [];
234+
let ExclusiveStartKey: Record<string, any> | undefined;
233235
const supplierStatus = `${supplierId}#${status}`;
234-
const result = await this.ddbClient.send(
235-
new QueryCommand({
236-
TableName: this.config.lettersTableName,
237-
IndexName: "supplierStatus-index",
238-
KeyConditionExpression: "supplierStatus = :supplierStatus",
239-
Limit: limit,
240-
ExpressionAttributeNames: {
241-
"#status": "status", // reserved keyword
242-
},
243-
ExpressionAttributeValues: {
244-
":supplierStatus": supplierStatus,
245-
},
246-
ProjectionExpression:
247-
"id, #status, specificationId, groupId, reasonCode, reasonText",
248-
}),
249-
);
250-
return z.array(LetterSchemaBase).parse(result.Items ?? []);
236+
let res;
237+
238+
do {
239+
const remaining = limit - items.length;
240+
241+
res = await this.ddbClient.send(
242+
new QueryCommand({
243+
TableName: this.config.lettersTableName,
244+
IndexName: "supplierStatus-index",
245+
KeyConditionExpression: "supplierStatus = :supplierStatus",
246+
ExpressionAttributeNames: {
247+
"#status": "status", // reserved keyword
248+
},
249+
ExpressionAttributeValues: {
250+
":supplierStatus": supplierStatus,
251+
},
252+
ProjectionExpression:
253+
"id, #status, specificationId, groupId, reasonCode, reasonText",
254+
Limit: remaining, // limit is a per-page cap
255+
ExclusiveStartKey,
256+
}),
257+
);
258+
259+
if (res.Items?.length) {
260+
items.push(...res.Items);
261+
}
262+
263+
ExclusiveStartKey = res.LastEvaluatedKey;
264+
} while (res.LastEvaluatedKey && items.length < limit);
265+
266+
return z.array(LetterSchemaBase).parse(items);
251267
}
252268
}

lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe("letter-mapper", () => {
2727
`https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.PRINTED.${event.dataschemaversion}.schema.json`,
2828
);
2929
expect(event.dataschemaversion).toMatch(/1\.\d+\.\d+/);
30-
expect(event.subject).toBe("letter-origin/supplier-api/letter/id1");
30+
expect(event.subject).toBe("letter-origin/letter-rendering/letter/id1");
3131
expect(event.time).toBe("2025-11-24T15:55:18.000Z");
3232
expect(event.recordedtime).toBe("2025-11-24T15:55:18.000Z");
3333
expect(event.data).toEqual({
@@ -40,7 +40,7 @@ describe("letter-mapper", () => {
4040
reasonCode: "R02",
4141
reasonText: "Reason text",
4242
origin: {
43-
domain: "supplier-api",
43+
domain: "letter-rendering",
4444
source: "letter-rendering/source/test",
4545
subject: "letter-rendering/source/letter/letter-id",
4646
event: event.id,

0 commit comments

Comments
 (0)