Skip to content

Commit ee3c095

Browse files
type checking up front
1 parent 8e8fc86 commit ee3c095

2 files changed

Lines changed: 70 additions & 41 deletions

File tree

lambdas/upsert-letter/src/handler/__tests__/upsert-handler.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,10 @@ describe("createUpsertLetterHandler", () => {
224224
const evt: SQSEvent = createSQSEvent([
225225
createSqsRecord("msg1", JSON.stringify(v2message)),
226226
createSqsRecord("msg2", JSON.stringify(v1message)),
227-
createSqsRecord("msg3", JSON.stringify(updateMessage)),
227+
createSqsRecord(
228+
"msg3",
229+
JSON.stringify(createSupplierStatusChangeEvent()),
230+
),
228231
]);
229232

230233
const result = await createUpsertLetterHandler(mockedDeps)(
@@ -425,7 +428,7 @@ describe("createUpsertLetterHandler", () => {
425428
);
426429
expect(mockMetrics.setNamespace).toHaveBeenCalledWith("upsertLetter");
427430
expect(mockMetrics.putDimensions).toHaveBeenCalledWith({
428-
Supplier: "supplier1",
431+
Supplier: "unknown",
429432
});
430433
expect(mockMetrics.putMetric).toHaveBeenCalledWith(
431434
"MessageFailed",

lambdas/upsert-letter/src/handler/upsert-handler.ts

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,27 @@ import { Deps } from "../config/deps";
1919
type SupplierSpec = { supplierId: string; specId: string };
2020
type PreparedEvents = LetterRequestPreparedEventV2 | LetterRequestPreparedEvent;
2121

22+
const SupplierSpecSchema = z.object({
23+
supplierId: z.string().min(1),
24+
specId: z.string().min(1),
25+
});
26+
27+
const LetterEventUnionSchema = z.discriminatedUnion("type", [
28+
$LetterRequestPreparedEventV2,
29+
$LetterRequestPreparedEvent,
30+
$LetterEvent,
31+
]);
32+
33+
const QueueMessageSchema = z.union([
34+
$LetterEvent,
35+
z.object({
36+
letterEvent: LetterEventUnionSchema,
37+
supplierSpec: SupplierSpecSchema.optional(),
38+
}),
39+
]);
40+
2241
type UpsertOperation = {
23-
name: "Insert" | "Update";
42+
name: "Insert" | "Update" | "Unknown";
2443
schemas: z.ZodSchema[];
2544
handler: (
2645
request: unknown,
@@ -33,7 +52,9 @@ type UpsertOperation = {
3352
const TypeEnvelope = z.object({ type: z.string().min(1) });
3453

3554
function getOperationFromType(type: string): UpsertOperation {
36-
if (type.startsWith("uk.nhs.notify.letter-rendering.letter-request.prepared"))
55+
if (
56+
type.startsWith("uk.nhs.notify.letter-rendering.letter-request.prepared")
57+
) {
3758
return {
3859
name: "Insert",
3960
schemas: [$LetterRequestPreparedEventV2, $LetterRequestPreparedEvent],
@@ -55,24 +76,24 @@ function getOperationFromType(type: string): UpsertOperation {
5576
});
5677
},
5778
};
58-
if (type.startsWith("uk.nhs.notify.supplier-api.letter"))
59-
return {
60-
name: "Update",
61-
schemas: [$LetterEvent],
62-
handler: async (request, supplierSpec, deps) => {
63-
const supplierEvent = request as LetterEvent;
64-
const letterToUpdate: UpdateLetter = mapToUpdateLetter(supplierEvent);
65-
await deps.letterRepo.updateLetterStatus(letterToUpdate);
79+
}
80+
// if it's not an insert type, it must be an update as we've already parsed the message, but we want to have a separate operation for better logging and metrics
81+
return {
82+
name: "Update",
83+
schemas: [$LetterEvent],
84+
handler: async (request, supplierSpec, deps) => {
85+
const supplierEvent = request as LetterEvent;
86+
const letterToUpdate: UpdateLetter = mapToUpdateLetter(supplierEvent);
87+
await deps.letterRepo.updateLetterStatus(letterToUpdate);
6688

67-
deps.logger.info({
68-
description: "Updated letter",
69-
eventId: supplierEvent.id,
70-
letterId: letterToUpdate.id,
71-
supplierId: letterToUpdate.supplierId,
72-
});
73-
},
74-
};
75-
throw new Error(`Unknown operation from type=${type}`);
89+
deps.logger.info({
90+
description: "Updated letter",
91+
eventId: supplierEvent.id,
92+
letterId: letterToUpdate.id,
93+
supplierId: letterToUpdate.supplierId,
94+
});
95+
},
96+
};
7697
}
7798

7899
function mapToInsertLetter(
@@ -111,19 +132,6 @@ function mapToUpdateLetter(upsertRequest: LetterEvent): UpdateLetter {
111132
reasonText: upsertRequest.data.reasonText,
112133
};
113134
}
114-
function getType(event: unknown) {
115-
const env = TypeEnvelope.safeParse(event);
116-
if (!env.success) {
117-
// Helpful debugging info:
118-
const pretty = (() => {
119-
return JSON.stringify(event, null, 2);
120-
})();
121-
throw new Error(
122-
`Missing or invalid envelope.type field. Payload seen:\n${pretty}`,
123-
);
124-
}
125-
return env.data.type;
126-
}
127135

128136
async function runUpsert(
129137
operation: UpsertOperation,
@@ -138,8 +146,6 @@ async function runUpsert(
138146
return;
139147
}
140148
}
141-
// none matched
142-
throw new Error("No matching schema for received message");
143149
}
144150

145151
async function emitMetrics(
@@ -193,7 +199,24 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler {
193199
queueMessage,
194200
});
195201

196-
const { letterEvent, supplierSpec } = queueMessage;
202+
const result = QueueMessageSchema.safeParse(queueMessage);
203+
if (!result.success) {
204+
throw new Error(
205+
`Message did not match expected schema: ${JSON.stringify(
206+
result.error.issues,
207+
)}`,
208+
);
209+
}
210+
let letterEvent: any;
211+
let supplierSpec: SupplierSpec | undefined;
212+
213+
if ("letterEvent" in result.data) {
214+
letterEvent = result.data.letterEvent;
215+
supplierSpec = result.data.supplierSpec;
216+
} else {
217+
letterEvent = result.data;
218+
supplierSpec = undefined;
219+
}
197220

198221
deps.logger.info({
199222
description: "Extracted letter event",
@@ -207,11 +230,14 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler {
207230
? getSupplierIdFromEvent(letterEvent)
208231
: supplierSpec.supplierId;
209232

210-
const type = getType(letterEvent);
233+
const operation = getOperationFromType(letterEvent.type);
211234

212-
const operation = getOperationFromType(type);
213-
214-
await runUpsert(operation, letterEvent, supplierSpec, deps);
235+
await runUpsert(
236+
operation,
237+
letterEvent,
238+
supplierSpec ?? { supplierId: "unknown", specId: "unknown" },
239+
deps,
240+
);
215241

216242
perSupplierSuccess.set(
217243
supplier,

0 commit comments

Comments
 (0)