Skip to content

Commit a8fa07e

Browse files
Split upsert operations
1 parent 78ece30 commit a8fa07e

File tree

7 files changed

+276
-407
lines changed

7 files changed

+276
-407
lines changed

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

Lines changed: 1 addition & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
setupDynamoDBContainer,
88
} from "./db";
99
import { LetterRepository } from "../letter-repository";
10-
import { InsertLetter, Letter, UpdateLetter, UpsertLetter } from "../types";
10+
import { InsertLetter, Letter, UpdateLetter } from "../types";
1111
import { LogStream, createTestLogger } from "./logs";
1212

1313
function createLetter(
@@ -484,149 +484,4 @@ describe("LetterRepository", () => {
484484
]),
485485
).rejects.toThrow("Cannot do operations on a non-existent table");
486486
});
487-
488-
test("successful upsert (update status) returns updated letter", async () => {
489-
const letter: InsertLetter = createLetter(
490-
"supplier1",
491-
"letter1",
492-
"PENDING",
493-
new Date(2020, 0, 1).toISOString(),
494-
);
495-
const existingLetter: Letter = await letterRepository.putLetter(letter);
496-
497-
const updateLetterStatus: UpsertLetter = {
498-
id: "letter1",
499-
supplierId: "supplier1",
500-
status: "REJECTED",
501-
reasonCode: "R01",
502-
reasonText: "R01 text",
503-
source: "/data-plane/letter-rendering/pdf",
504-
subject: "client/1/letter-request/letter1",
505-
};
506-
507-
const before = Date.now();
508-
509-
const result: Letter =
510-
await letterRepository.upsertLetter(updateLetterStatus);
511-
512-
const after = Date.now();
513-
514-
expect(result).toEqual(
515-
expect.objectContaining({
516-
id: "letter1",
517-
status: "REJECTED",
518-
specificationId: "specification1",
519-
groupId: "group1",
520-
reasonCode: "R01",
521-
reasonText: "R01 text",
522-
supplierId: "supplier1",
523-
url: "s3://bucket/letter1.pdf",
524-
supplierStatus: "supplier1#REJECTED",
525-
source: "/data-plane/letter-rendering/pdf",
526-
subject: "client/1/letter-request/letter1",
527-
}),
528-
);
529-
expect(Date.parse(result.updatedAt)).toBeGreaterThan(
530-
Date.parse(existingLetter.updatedAt),
531-
);
532-
expect(result.createdAt).toBe(existingLetter.createdAt);
533-
expect(result.createdAt).toBe(result.supplierStatusSk);
534-
assertTtl(result.ttl, before, after);
535-
});
536-
537-
test("successful upsert (insert letter) returns created letter", async () => {
538-
const insertLetter: UpsertLetter = {
539-
id: "letter1",
540-
status: "PENDING",
541-
specificationId: "specification1",
542-
groupId: "group1",
543-
supplierId: "supplier1",
544-
url: "s3://bucket/letter1.pdf",
545-
source: "/data-plane/letter-rendering/pdf",
546-
subject: "client/1/letter-request/letter1",
547-
};
548-
549-
const before = Date.now();
550-
551-
const result: Letter = await letterRepository.upsertLetter(insertLetter);
552-
553-
const after = Date.now();
554-
555-
expect(result).toEqual(
556-
expect.objectContaining({
557-
id: "letter1",
558-
status: "PENDING",
559-
specificationId: "specification1",
560-
groupId: "group1",
561-
supplierId: "supplier1",
562-
url: "s3://bucket/letter1.pdf",
563-
source: "/data-plane/letter-rendering/pdf",
564-
subject: "client/1/letter-request/letter1",
565-
}),
566-
);
567-
568-
expect(Date.parse(result.createdAt)).toBeGreaterThanOrEqual(before);
569-
expect(Date.parse(result.createdAt)).toBeLessThanOrEqual(after);
570-
expect(result.updatedAt).toBe(result.createdAt);
571-
expect(result.supplierStatusSk).toBe(result.createdAt);
572-
assertTtl(result.ttl, before, after);
573-
});
574-
575-
test("successful upsert without status change (update url)", async () => {
576-
const insertLetter: InsertLetter = createLetter(
577-
"supplier1",
578-
"letter1",
579-
"PENDING",
580-
new Date(2020, 0, 1).toISOString(),
581-
);
582-
const existingLetter = await letterRepository.putLetter(insertLetter);
583-
584-
const before = Date.now();
585-
586-
const result = await letterRepository.upsertLetter({
587-
id: "letter1",
588-
supplierId: "supplier1",
589-
url: "s3://updateToPdf",
590-
});
591-
592-
const after = Date.now();
593-
594-
expect(result).toEqual(
595-
expect.objectContaining({
596-
id: "letter1",
597-
status: "PENDING",
598-
specificationId: "specification1",
599-
groupId: "group1",
600-
supplierId: "supplier1",
601-
url: "s3://updateToPdf",
602-
supplierStatus: "supplier1#PENDING",
603-
source: "/data-plane/letter-rendering/pdf",
604-
subject: "client/1/letter-request/letter1",
605-
}),
606-
);
607-
expect(Date.parse(result.updatedAt)).toBeGreaterThan(
608-
Date.parse(existingLetter.updatedAt),
609-
);
610-
expect(result.createdAt).toBe(existingLetter.createdAt);
611-
expect(result.createdAt).toBe(result.supplierStatusSk);
612-
assertTtl(result.ttl, before, after);
613-
});
614-
615-
test("unsuccessful upsert should throw error", async () => {
616-
const mockSend = jest.fn().mockResolvedValue({ Items: null });
617-
const mockDdbClient = { send: mockSend } as any;
618-
const repo = new LetterRepository(
619-
mockDdbClient,
620-
{ debug: jest.fn() } as any,
621-
{ lettersTableName: "letters", lettersTtlHours: 1 },
622-
);
623-
624-
await expect(
625-
repo.upsertLetter({
626-
id: "letter1",
627-
status: "PENDING",
628-
supplierId: "supplier1",
629-
}),
630-
).rejects.toThrow("upsertLetter: no attributes returned");
631-
});
632487
});

internal/datastore/src/letter-repository.ts

Lines changed: 0 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
LetterSchema,
1717
LetterSchemaBase,
1818
UpdateLetter,
19-
UpsertLetter,
2019
} from "./types";
2120

2221
export type PagingOptions = Partial<{
@@ -250,103 +249,4 @@ export class LetterRepository {
250249
);
251250
return z.array(LetterSchemaBase).parse(result.Items ?? []);
252251
}
253-
254-
async upsertLetter(upsert: UpsertLetter): Promise<Letter> {
255-
const now = new Date();
256-
const ttl = Math.floor(
257-
now.valueOf() / 1000 + 60 * 60 * this.config.lettersTtlHours,
258-
);
259-
260-
const setParts: string[] = [];
261-
const exprAttrNames: Record<string, string> = {};
262-
const exprAttrValues: Record<string, any> = {};
263-
264-
// updateAt is always updated
265-
setParts.push("updatedAt = :updatedAt");
266-
exprAttrValues[":updatedAt"] = now.toISOString();
267-
268-
// ttl is always updated
269-
setParts.push("#ttl = :ttl");
270-
exprAttrNames["#ttl"] = "ttl";
271-
exprAttrValues[":ttl"] = ttl;
272-
273-
// createdAt only if first time
274-
setParts.push("createdAt = if_not_exists(createdAt, :createdAt)");
275-
exprAttrValues[":createdAt"] = now.toISOString();
276-
277-
// status and related supplierStatus if provided
278-
if (upsert.status !== undefined) {
279-
exprAttrNames["#status"] = "status";
280-
setParts.push("#status = :status");
281-
exprAttrValues[":status"] = upsert.status;
282-
283-
setParts.push("supplierStatus = :supplierStatus");
284-
exprAttrValues[":supplierStatus"] =
285-
`${upsert.supplierId}#${upsert.status}`;
286-
287-
// supplierStatusSk should replicate createdAt
288-
setParts.push(
289-
"supplierStatusSk = if_not_exists(supplierStatusSk, :supplierStatusSk)",
290-
);
291-
exprAttrValues[":supplierStatusSk"] = now.toISOString();
292-
}
293-
294-
// fields that could be updated
295-
296-
if (upsert.specificationId !== undefined) {
297-
setParts.push("specificationId = :specificationId");
298-
exprAttrValues[":specificationId"] = upsert.specificationId;
299-
}
300-
301-
if (upsert.url !== undefined) {
302-
setParts.push("#url = :url");
303-
exprAttrNames["#url"] = "url";
304-
exprAttrValues[":url"] = upsert.url;
305-
}
306-
307-
if (upsert.groupId !== undefined) {
308-
setParts.push("groupId = :groupId");
309-
exprAttrValues[":groupId"] = upsert.groupId;
310-
}
311-
312-
if (upsert.reasonCode !== undefined) {
313-
setParts.push("reasonCode = :reasonCode");
314-
exprAttrValues[":reasonCode"] = upsert.reasonCode;
315-
}
316-
if (upsert.reasonText !== undefined) {
317-
setParts.push("reasonText = :reasonText");
318-
exprAttrValues[":reasonText"] = upsert.reasonText;
319-
}
320-
321-
if (upsert.source !== undefined) {
322-
setParts.push("#source = :source");
323-
exprAttrNames["#source"] = "source";
324-
exprAttrValues[":source"] = upsert.source;
325-
}
326-
327-
if (upsert.subject !== undefined) {
328-
setParts.push("subject = :subject");
329-
exprAttrValues[":subject"] = upsert.subject;
330-
}
331-
332-
const updateExpression = `SET ${setParts.join(", ")}`;
333-
334-
const command = new UpdateCommand({
335-
TableName: this.config.lettersTableName,
336-
Key: { supplierId: upsert.supplierId, id: upsert.id },
337-
UpdateExpression: updateExpression,
338-
ExpressionAttributeNames: exprAttrNames,
339-
ExpressionAttributeValues: exprAttrValues,
340-
ReturnValues: "ALL_NEW",
341-
});
342-
343-
const result = await this.ddbClient.send(command);
344-
345-
if (!result.Attributes) {
346-
throw new Error("upsertLetter: no attributes returned");
347-
}
348-
349-
this.log.debug({ exprAttrValues }, `Upsert to letter=${upsert.id}`);
350-
return LetterSchema.parse(result.Attributes);
351-
}
352252
}

internal/datastore/src/types.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,6 @@ export type UpdateLetter = {
7171
reasonCode?: string;
7272
reasonText?: string;
7373
};
74-
export type UpsertLetter = {
75-
id: string;
76-
supplierId: string;
77-
// fields that might set/overwrite
78-
status?: Letter["status"];
79-
specificationId?: string;
80-
groupId?: string;
81-
url?: string;
82-
reasonCode?: string;
83-
reasonText?: string;
84-
source?: string;
85-
subject?: string;
86-
};
8774

8875
export const MISchemaBase = z.object({
8976
id: z.string(),

lambdas/upsert-letter/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"@internal/datastore": "*",
66
"@nhsdigital/nhs-notify-event-schemas-letter-rendering": "^2.0.0",
77
"@nhsdigital/nhs-notify-event-schemas-letter-rendering-v1": "npm:@nhsdigital/nhs-notify-event-schemas-letter-rendering@^1.1.5",
8+
"@nhsdigital/nhs-notify-event-schemas-supplier-api": "^1.0.5",
89
"@types/aws-lambda": "^8.10.148",
910
"aws-lambda": "^1.0.7",
1011
"esbuild": "^0.24.0",

0 commit comments

Comments
 (0)