Skip to content

Commit c242e72

Browse files
authored
Merge branch 'main' into dependabotCombined
2 parents 43de0f9 + 4400b7e commit c242e72

File tree

13 files changed

+325
-94
lines changed

13 files changed

+325
-94
lines changed

infrastructure/terraform/components/api/ddb_table_letter_queue.tf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ resource "aws_dynamodb_table" "letter_queue" {
33
billing_mode = "PAY_PER_REQUEST"
44

55
hash_key = "supplierId"
6-
range_key = "queueTimestamp"
6+
range_key = "letterId"
77

88
ttl {
99
attribute_name = "ttl"
1010
enabled = true
1111
}
1212

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

infrastructure/terraform/components/api/module_lambda_update_letter_queue.tf

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ module "update_letter_queue" {
3535
log_subscription_role_arn = local.acct.log_subscription_role_arn
3636

3737
lambda_env_vars = merge(local.common_lambda_env_vars, {
38-
LETTER_QUEUE_TABLE_NAME = aws_dynamodb_table.letter_queue.name,
38+
LETTER_QUEUE_TABLE_NAME = "${local.csi}-letter-queue",
3939
LETTER_QUEUE_TTL_HOURS = 168 # 7 days
4040
})
4141
}
@@ -47,11 +47,12 @@ data "aws_iam_policy_document" "update_letter_queue_lambda" {
4747

4848
actions = [
4949
"dynamodb:PutItem",
50+
"dynamodb:DeleteItem",
5051
]
5152

5253
resources = [
53-
aws_dynamodb_table.letter_queue.arn,
54-
"${aws_dynamodb_table.letter_queue.arn}/index/*"
54+
"arn:aws:dynamodb:${var.region}:${var.aws_account_id}:table/${local.csi}-letter-queue",
55+
"arn:aws:dynamodb:${var.region}:${var.aws_account_id}:table/${local.csi}-letter-queue/index/*"
5556
]
5657
}
5758

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: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { GetCommand } from "@aws-sdk/lib-dynamodb";
12
import { Logger } from "pino";
23
import {
34
DBContext,
@@ -7,8 +8,9 @@ import {
78
} from "./db";
89
import LetterQueueRepository from "../letter-queue-repository";
910
import { InsertPendingLetter } from "../types";
10-
import { LetterAlreadyExistsError } from "../errors";
11+
import { LetterAlreadyExistsError } from "../letter-already-exists-error";
1112
import { createTestLogger } from "./logs";
13+
import { LetterDoesNotExistError } from "../letter-does-not-exist-error";
1214

1315
function createLetter(letterId = "letter1"): InsertPendingLetter {
1416
return {
@@ -51,32 +53,19 @@ describe("LetterQueueRepository", () => {
5153
await db.container.stop();
5254
});
5355

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

6960
const pendingLetter =
7061
await letterQueueRepository.putLetter(createLetter());
7162

72-
const after = Date.now();
73-
74-
const timestampInMillis = new Date(
75-
pendingLetter.queueTimestamp,
76-
).valueOf();
77-
expect(timestampInMillis).toBeGreaterThanOrEqual(before);
78-
expect(timestampInMillis).toBeLessThanOrEqual(after);
79-
assertTtl(pendingLetter.ttl, before, after);
63+
expect(pendingLetter.queueTimestamp).toBe("2026-03-04T13:15:45.000Z");
64+
expect(pendingLetter.visibilityTimestamp).toBe(
65+
"2026-03-04T13:15:45.000Z",
66+
);
67+
expect(pendingLetter.ttl).toBe(1_772_633_745);
68+
expect(await letterExists(db, "supplier1", "letter1")).toBe(true);
8069
});
8170

8271
it("throws LetterAlreadyExistsError when creating a letter which already exists", async () => {
@@ -101,4 +90,48 @@ describe("LetterQueueRepository", () => {
10190
).rejects.toThrow("Cannot do operations on a non-existent table");
10291
});
10392
});
93+
94+
describe("deleteLetter", () => {
95+
it("deletes a letter from the database", async () => {
96+
await letterQueueRepository.putLetter(createLetter());
97+
98+
await letterQueueRepository.deleteLetter("supplier1", "letter1");
99+
100+
expect(await letterExists(db, "supplier1", "letter1")).toBe(false);
101+
});
102+
103+
it("throws an error when the letter does not exist", async () => {
104+
await expect(
105+
letterQueueRepository.deleteLetter("supplier1", "letter1"),
106+
).rejects.toThrow(LetterDoesNotExistError);
107+
});
108+
109+
it("rethrows errors from DynamoDB when deleting a letter", async () => {
110+
const misconfiguredRepository = new LetterQueueRepository(
111+
db.docClient,
112+
logger,
113+
{
114+
...db.config,
115+
letterQueueTableName: "nonexistent-table",
116+
},
117+
);
118+
await expect(
119+
misconfiguredRepository.deleteLetter("supplier1", "letter1"),
120+
).rejects.toThrow("Cannot do operations on a non-existent table");
121+
});
122+
});
104123
});
124+
125+
async function letterExists(
126+
db: DBContext,
127+
supplierId: string,
128+
letterId: string,
129+
): Promise<boolean> {
130+
const result = await db.docClient.send(
131+
new GetCommand({
132+
TableName: db.config.letterQueueTableName,
133+
Key: { supplierId, letterId },
134+
}),
135+
);
136+
return result.Item !== undefined;
137+
}

internal/datastore/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./types";
2-
export * from "./errors";
2+
export * from "./letter-already-exists-error";
3+
export * from "./letter-does-not-exist-error";
34
export * from "./mi-repository";
45
export * from "./letter-repository";
56
export * from "./supplier-repository";

internal/datastore/src/errors.ts renamed to internal/datastore/src/letter-already-exists-error.ts

File renamed without changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Error thrown when attempting to delete a letter that does not exist in the database.
3+
*/
4+
// eslint-disable-next-line import-x/prefer-default-export
5+
export class LetterDoesNotExistError extends Error {
6+
constructor(
7+
public readonly supplierId: string,
8+
public readonly letterId: string,
9+
) {
10+
super(
11+
`Letter does not exist: supplierId=${supplierId}, letterId=${letterId}`,
12+
);
13+
this.name = "LetterDoesNotExistError";
14+
}
15+
}

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
1+
import {
2+
DeleteCommand,
3+
DynamoDBDocumentClient,
4+
PutCommand,
5+
} from "@aws-sdk/lib-dynamodb";
26
import { Logger } from "pino";
37
import {
48
InsertPendingLetter,
59
PendingLetter,
610
PendingLetterSchema,
711
} from "./types";
8-
import { LetterAlreadyExistsError } from "./errors";
12+
import { LetterAlreadyExistsError } from "./letter-already-exists-error";
13+
import { LetterDoesNotExistError } from "./letter-does-not-exist-error";
914

1015
type LetterQueueRepositoryConfig = {
1116
letterQueueTableName: string;
@@ -22,10 +27,13 @@ export default class LetterQueueRepository {
2227
async putLetter(
2328
insertPendingLetter: InsertPendingLetter,
2429
): Promise<PendingLetter> {
30+
// needs to be an ISO timestamp as Db sorts alphabetically
31+
const now = new Date().toISOString();
32+
2533
const pendingLetter: PendingLetter = {
2634
...insertPendingLetter,
27-
// needs to be an ISO timestamp as Db sorts alphabetically
28-
queueTimestamp: new Date().toISOString(),
35+
queueTimestamp: now,
36+
visibilityTimestamp: now,
2937
ttl: Math.floor(
3038
Date.now() / 1000 + 60 * 60 * this.config.letterQueueTtlHours,
3139
),
@@ -52,4 +60,24 @@ export default class LetterQueueRepository {
5260
}
5361
return PendingLetterSchema.parse(pendingLetter);
5462
}
63+
64+
async deleteLetter(supplierId: string, letterId: string): Promise<void> {
65+
try {
66+
await this.ddbClient.send(
67+
new DeleteCommand({
68+
TableName: this.config.letterQueueTableName,
69+
Key: { supplierId, letterId },
70+
ConditionExpression: "attribute_exists(letterId)",
71+
}),
72+
);
73+
} catch (error) {
74+
if (
75+
error instanceof Error &&
76+
error.name === "ConditionalCheckFailedException"
77+
) {
78+
throw new LetterDoesNotExistError(supplierId, letterId);
79+
}
80+
throw error;
81+
}
82+
}
5583
}

internal/datastore/src/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,18 @@ export const PendingLetterSchema = z.object({
8080
supplierId: idRef(SupplierSchema, "id"),
8181
letterId: idRef(LetterSchema, "id"),
8282
queueTimestamp: z.string().describe("Secondary index SK"),
83+
visibilityTimestamp: z.string(),
8384
specificationId: z.string(),
8485
groupId: z.string(),
8586
ttl: z.int(),
8687
});
8788

8889
export type PendingLetter = z.infer<typeof PendingLetterSchema>;
8990

90-
export type InsertPendingLetter = Omit<PendingLetter, "ttl" | "queueTimestamp">;
91+
export type InsertPendingLetter = Omit<
92+
PendingLetter,
93+
"ttl" | "queueTimestamp" | "visibilityTimestamp"
94+
>;
9195

9296
export const MISchemaBase = z.object({
9397
id: z.string(),

0 commit comments

Comments
 (0)