Skip to content

Commit 9e00f62

Browse files
type checking up front
1 parent 8e8fc86 commit 9e00f62

2 files changed

Lines changed: 70 additions & 47 deletions

File tree

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,14 +217,14 @@ describe("createUpsertLetterHandler", () => {
217217
letterEvent: createPreparedV1Event(),
218218
supplierSpec: { supplierId: "supplier1", specId: "spec1" },
219219
};
220-
const updateMessage = {
221-
letterEvent: createSupplierStatusChangeEvent(),
222-
};
223220

224221
const evt: SQSEvent = createSQSEvent([
225222
createSqsRecord("msg1", JSON.stringify(v2message)),
226223
createSqsRecord("msg2", JSON.stringify(v1message)),
227-
createSqsRecord("msg3", JSON.stringify(updateMessage)),
224+
createSqsRecord(
225+
"msg3",
226+
JSON.stringify(createSupplierStatusChangeEvent()),
227+
),
228228
]);
229229

230230
const result = await createUpsertLetterHandler(mockedDeps)(
@@ -425,7 +425,7 @@ describe("createUpsertLetterHandler", () => {
425425
);
426426
expect(mockMetrics.setNamespace).toHaveBeenCalledWith("upsertLetter");
427427
expect(mockMetrics.putDimensions).toHaveBeenCalledWith({
428-
Supplier: "supplier1",
428+
Supplier: "unknown",
429429
});
430430
expect(mockMetrics.putMetric).toHaveBeenCalledWith(
431431
"MessageFailed",

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

Lines changed: 65 additions & 42 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,
@@ -29,11 +48,10 @@ type UpsertOperation = {
2948
) => Promise<void>;
3049
};
3150

32-
// small envelope that must exist in all inputs
33-
const TypeEnvelope = z.object({ type: z.string().min(1) });
34-
3551
function getOperationFromType(type: string): UpsertOperation {
36-
if (type.startsWith("uk.nhs.notify.letter-rendering.letter-request.prepared"))
52+
if (
53+
type.startsWith("uk.nhs.notify.letter-rendering.letter-request.prepared")
54+
) {
3755
return {
3856
name: "Insert",
3957
schemas: [$LetterRequestPreparedEventV2, $LetterRequestPreparedEvent],
@@ -55,24 +73,24 @@ function getOperationFromType(type: string): UpsertOperation {
5573
});
5674
},
5775
};
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);
76+
}
77+
// 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
78+
return {
79+
name: "Update",
80+
schemas: [$LetterEvent],
81+
handler: async (request, supplierSpec, deps) => {
82+
const supplierEvent = request as LetterEvent;
83+
const letterToUpdate: UpdateLetter = mapToUpdateLetter(supplierEvent);
84+
await deps.letterRepo.updateLetterStatus(letterToUpdate);
6685

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}`);
86+
deps.logger.info({
87+
description: "Updated letter",
88+
eventId: supplierEvent.id,
89+
letterId: letterToUpdate.id,
90+
supplierId: letterToUpdate.supplierId,
91+
});
92+
},
93+
};
7694
}
7795

7896
function mapToInsertLetter(
@@ -111,19 +129,6 @@ function mapToUpdateLetter(upsertRequest: LetterEvent): UpdateLetter {
111129
reasonText: upsertRequest.data.reasonText,
112130
};
113131
}
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-
}
127132

128133
async function runUpsert(
129134
operation: UpsertOperation,
@@ -138,8 +143,6 @@ async function runUpsert(
138143
return;
139144
}
140145
}
141-
// none matched
142-
throw new Error("No matching schema for received message");
143146
}
144147

145148
async function emitMetrics(
@@ -193,7 +196,24 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler {
193196
queueMessage,
194197
});
195198

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

198218
deps.logger.info({
199219
description: "Extracted letter event",
@@ -207,11 +227,14 @@ export default function createUpsertLetterHandler(deps: Deps): SQSHandler {
207227
? getSupplierIdFromEvent(letterEvent)
208228
: supplierSpec.supplierId;
209229

210-
const type = getType(letterEvent);
230+
const operation = getOperationFromType(letterEvent.type);
211231

212-
const operation = getOperationFromType(type);
213-
214-
await runUpsert(operation, letterEvent, supplierSpec, deps);
232+
await runUpsert(
233+
operation,
234+
letterEvent,
235+
supplierSpec ?? { supplierId: "unknown", specId: "unknown" },
236+
deps,
237+
);
215238

216239
perSupplierSuccess.set(
217240
supplier,

0 commit comments

Comments
 (0)