From c4188ef6270eb5f9458cdc2d6703807dc0f02a91 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Mon, 23 Mar 2026 13:53:36 +0000 Subject: [PATCH 01/17] CCM-14616: POC --- .../__tests__/apis/sqs-trigger-lambda.test.ts | 4 +- .../src/apis/sqs-trigger-lambda.ts | 18 +++--- src/digital-letters-events/.gitignore | 1 + src/digital-letters-events/index.ts | 2 + src/digital-letters-events/package.json | 2 +- src/typescript-schema-generator/package.json | 3 +- .../src/generate-guard-functions-cli.ts | 8 +++ .../src/generate-guard-functions.ts | 55 +++++++++++++++++++ .../src/generate-types.ts | 7 ++- 9 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 src/digital-letters-events/index.ts create mode 100644 src/typescript-schema-generator/src/generate-guard-functions-cli.ts create mode 100644 src/typescript-schema-generator/src/generate-guard-functions.ts diff --git a/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts index e2320758b..49f50f743 100644 --- a/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts +++ b/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts @@ -85,7 +85,9 @@ describe('createHandler', () => { expect(res.batchItemFailures).toEqual([{ itemIdentifier: 'msg2' }]); expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ - description: expect.stringContaining('parsing ttl queue entry'), + description: expect.stringContaining( + 'Error parsing MESHInboxMessageDownloaded event', + ), }), ); expect(logger.info).toHaveBeenCalledWith({ diff --git a/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts b/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts index 5c9a91ab5..1f1fc1afe 100644 --- a/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts @@ -7,10 +7,10 @@ import { randomUUID } from 'node:crypto'; import type { CreateTtl, CreateTtlOutcome } from 'app/create-ttl'; import { EventPublisher, Logger } from 'utils'; import itemEnqueuedValidator from 'digital-letters-events/ItemEnqueued.js'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; import { ItemEnqueued, MESHInboxMessageDownloaded, + validateMESHInboxMessageDownloaded, } from 'digital-letters-events'; interface ProcessingResult { @@ -36,19 +36,15 @@ export const createHandler = ({ async ({ body, messageId }): Promise => { try { const sqsEventBody = JSON.parse(body); - const sqsEventDetail = sqsEventBody.detail; - - const isEventValid = messageDownloadedValidator(sqsEventDetail); - if (!isEventValid) { - logger.error({ - err: messageDownloadedValidator.errors, - description: 'Error parsing ttl queue entry', - }); + const messageDownloadedEvent = validateMESHInboxMessageDownloaded( + sqsEventBody.detail, + logger, + ); + + if (!messageDownloadedEvent) { batchItemFailures.push({ itemIdentifier: messageId }); return { result: 'failed' }; } - const messageDownloadedEvent: MESHInboxMessageDownloaded = - sqsEventDetail; const result = await createTtl.send(messageDownloadedEvent); diff --git a/src/digital-letters-events/.gitignore b/src/digital-letters-events/.gitignore index 808d18e93..e26513150 100644 --- a/src/digital-letters-events/.gitignore +++ b/src/digital-letters-events/.gitignore @@ -1,3 +1,4 @@ validators types models +guard-functions diff --git a/src/digital-letters-events/index.ts b/src/digital-letters-events/index.ts new file mode 100644 index 000000000..804f8e484 --- /dev/null +++ b/src/digital-letters-events/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './guard-functions'; diff --git a/src/digital-letters-events/package.json b/src/digital-letters-events/package.json index 7b8327cef..c482fa68e 100644 --- a/src/digital-letters-events/package.json +++ b/src/digital-letters-events/package.json @@ -10,7 +10,7 @@ }, "exports": { ".": { - "types": "./types/index.d.ts" + "default": "./index.ts" }, "./*.js": { "default": "./validators/*.js", diff --git a/src/typescript-schema-generator/package.json b/src/typescript-schema-generator/package.json index 5059875e6..dc988426c 100644 --- a/src/typescript-schema-generator/package.json +++ b/src/typescript-schema-generator/package.json @@ -18,7 +18,8 @@ "name": "typescript-schema-generator", "private": true, "scripts": { - "generate-dependencies": "npm run generate-types && npm run generate-validators", + "generate-dependencies": "npm run generate-types && npm run generate-validators && npm run generate-guard-functions", + "generate-guard-functions": "tsx src/generate-guard-functions-cli.ts", "generate-types": "tsx src/generate-types-cli.ts", "generate-validators": "tsx src/generate-validators-cli.ts", "lint": "eslint src", diff --git a/src/typescript-schema-generator/src/generate-guard-functions-cli.ts b/src/typescript-schema-generator/src/generate-guard-functions-cli.ts new file mode 100644 index 000000000..454b7555c --- /dev/null +++ b/src/typescript-schema-generator/src/generate-guard-functions-cli.ts @@ -0,0 +1,8 @@ +/* eslint-disable no-console */ + +import { generateGuardFunctions } from 'generate-guard-functions' + +generateGuardFunctions().catch((error) => { + console.error('Error generating guard functions:', error); + throw error; +}); diff --git a/src/typescript-schema-generator/src/generate-guard-functions.ts b/src/typescript-schema-generator/src/generate-guard-functions.ts new file mode 100644 index 000000000..246ab0e46 --- /dev/null +++ b/src/typescript-schema-generator/src/generate-guard-functions.ts @@ -0,0 +1,55 @@ +/* eslint-disable no-console */ +import { createOutputDir, writeFile, writeTypesIndex } from 'file-utils'; +import path from 'node:path'; +import { writeFileSync } from 'node:fs'; +import { eventSchemasDir, listEventSchemas, loadSchema } from 'utils'; + +export async function generateGuardFunctions() { + const eventSchemaFilenames = listEventSchemas(); + const outputDir = createOutputDir('guard-functions'); + console.log(`Output directory created at ${outputDir}`); + + console.group('Writing guard functions:'); + const indexLines: string[] = []; + for (const eventSchemaFilename of eventSchemaFilenames) { + const eventSchemaPath = path.join(eventSchemasDir, eventSchemaFilename); + const eventSchema = loadSchema(eventSchemaPath); + const typeName = eventSchema.title; + + const validatorVariableName = `event${typeName}Validator`; + + let guardFunction = `import ${validatorVariableName} from 'digital-letters-events/${typeName}.js'\n`; + guardFunction += `import type { ${typeName} } from 'digital-letters-events';\n`; + guardFunction += `import { Logger } from 'utils';\n\n`; + + guardFunction += `export function validate${typeName}(\n`; + guardFunction += ` event: unknown,\n`; + guardFunction += ` logger: Logger,\n`; + guardFunction += `): ${typeName} | null {\n\n`; + + guardFunction += ` const eventValidator = event${typeName}Validator as (d: unknown) => d is ${typeName};\n\n`; + + guardFunction += ` if (!eventValidator(event)) {\n`; + guardFunction += ` logger.error({\n`; + guardFunction += ` err: ${validatorVariableName}.errors,\n`; + guardFunction += ` description: 'Error parsing ${typeName} event',\n`; + guardFunction += ` });\n`; + guardFunction += ` return null;\n`; + guardFunction += ` }\n\n`; + + guardFunction += ` return event;\n`; + guardFunction += `}\n\n`; + + + const typeDeclarationName = `${typeName}`; + const typeDeclarationFilename = `${typeDeclarationName}.ts`; + writeFile(outputDir, typeDeclarationFilename, guardFunction); + console.log(typeDeclarationFilename); + + indexLines.push(`export * from './${typeDeclarationName}';`); + } + console.groupEnd(); + + writeFileSync(path.join(outputDir, 'index.ts'), `${indexLines.join('\n')}\n`); + console.log('index.ts file written'); +} diff --git a/src/typescript-schema-generator/src/generate-types.ts b/src/typescript-schema-generator/src/generate-types.ts index 81cc10a2f..6a38b3897 100644 --- a/src/typescript-schema-generator/src/generate-types.ts +++ b/src/typescript-schema-generator/src/generate-types.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { createOutputDir, writeFile, writeTypesIndex } from 'file-utils'; import { compile } from 'json-schema-to-typescript'; +import { writeFileSync } from 'node:fs'; import path from 'node:path'; import { eventSchemasDir, listEventSchemas, loadSchema } from 'utils'; @@ -21,7 +22,7 @@ export async function generateTypes() { }); // Write a .d.ts file named after the schema title or file. - const typeDeclarationFilename = `${typeName}.d.ts`; + const typeDeclarationFilename = `${typeName}.ts`; writeFile(outputDir, typeDeclarationFilename, eventTs); console.log(typeDeclarationFilename); @@ -30,6 +31,6 @@ export async function generateTypes() { } console.groupEnd(); - writeTypesIndex(outputDir, indexLines); - console.log('index.d.ts file written'); + writeFileSync(path.join(outputDir, 'index.ts'), `${indexLines.join('\n')}\n`); + console.log('index.ts file written'); } From a9b8f8f157db76becd78dbde7d42cbe8f2b03b5a Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Tue, 24 Mar 2026 15:56:00 +0000 Subject: [PATCH 02/17] CCM-13675: Event code generation improvements --- .../src/__tests__/apis/sqs-handler.test.ts | 8 ++--- .../__tests__/app/parse-sqs-message.test.ts | 33 +++++++++---------- .../src/app/parse-sqs-message.ts | 24 +++++--------- .../invalid-pdm-resource-available-event.ts | 8 ----- .../src/__tests__/apis/sqs-handler.test.ts | 4 +-- .../src/apis/sqs-handler.ts | 13 ++------ .../src/__tests__/apis/sqs-handler.test.ts | 8 ++--- .../pdm-poll-lambda/src/apis/sqs-handler.ts | 23 +++---------- .../__tests__/apis/sqs-trigger-lambda.test.ts | 2 +- .../src/apis/sqs-trigger-lambda.ts | 11 ++----- .../src/__tests__/apis/sqs-handler.test.ts | 12 +++---- .../print-analyser/src/apis/sqs-handler.ts | 21 +++++------- .../__tests__/apis/sqs-trigger-lambda.test.ts | 13 ++++++-- .../src/apis/sqs-trigger-lambda.ts | 22 ++++--------- .../__tests__/apis/sqs-trigger-lambda.test.ts | 2 +- .../src/apis/sqs-trigger-lambda.ts | 16 ++++----- .../src/apis/sqs-trigger-lambda.ts | 15 +++------ .../apis/dynamodb-stream-handler.test.ts | 4 +-- .../src/apis/dynamodb-stream-handler.ts | 24 +++----------- .../2025-10-draft/defs/print.schema.yaml | 1 - .../errors/InvalidEvent.ts | 8 +++++ src/digital-letters-events/errors/index.ts | 1 + src/digital-letters-events/index.ts | 1 + src/typescript-schema-generator/package.json | 3 +- .../src/generate-guard-functions.ts | 17 ++++------ utils/utils/src/logger.ts | 5 ++- 26 files changed, 114 insertions(+), 185 deletions(-) delete mode 100644 lambdas/core-notifier-lambda/src/domain/invalid-pdm-resource-available-event.ts create mode 100644 src/digital-letters-events/errors/InvalidEvent.ts create mode 100644 src/digital-letters-events/errors/index.ts diff --git a/lambdas/core-notifier-lambda/src/__tests__/apis/sqs-handler.test.ts b/lambdas/core-notifier-lambda/src/__tests__/apis/sqs-handler.test.ts index ceb3a6985..17e5b437f 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/apis/sqs-handler.test.ts @@ -5,10 +5,10 @@ import { NotifyMessageProcessor } from 'app/notify-message-processor'; import { ISenderManagement } from 'sender-management'; import { SqsHandlerDependencies, createHandler } from 'apis/sqs-handler'; import { parseSqsRecord } from 'app/parse-sqs-message'; -import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event'; import { RequestNotifyError } from 'domain/request-notify-error'; import { validPdmEvent, validSender } from '__tests__/constants'; import { + InvalidEvent, MessageRequestRejected, MessageRequestSkipped, MessageRequestSubmitted, @@ -195,14 +195,14 @@ describe('createHandler', () => { }); }); - describe('when parseSqsRecord throws InvalidPdmResourceAvailableEvent', () => { + describe('when parseSqsRecord throws InvalidEvent', () => { it('marks the message as failed for retry', async () => { const sqsEvent = createSqsEvent(1); const handler = createHandler(dependencies); const { messageId } = sqsEvent.Records[0]; mockParseSqsRecord.mockImplementationOnce(() => { - throw new InvalidPdmResourceAvailableEvent(messageId); + throw new InvalidEvent('Some validation errors'); }); const result = await handler(sqsEvent); @@ -211,7 +211,7 @@ describe('createHandler', () => { batchItemFailures: [{ itemIdentifier: messageId }], }); expect(mockLogger.warn).toHaveBeenCalledWith({ - error: 'Unable to parse PDMResourceAvailable event from SQS message', + error: 'Unable to parse event', description: 'Failed processing message', messageId, senderId: undefined, diff --git a/lambdas/core-notifier-lambda/src/__tests__/app/parse-sqs-message.test.ts b/lambdas/core-notifier-lambda/src/__tests__/app/parse-sqs-message.test.ts index a99eba2cd..d9040da98 100644 --- a/lambdas/core-notifier-lambda/src/__tests__/app/parse-sqs-message.test.ts +++ b/lambdas/core-notifier-lambda/src/__tests__/app/parse-sqs-message.test.ts @@ -2,10 +2,11 @@ import type { SQSRecord } from 'aws-lambda'; import { mock } from 'jest-mock-extended'; import { Logger } from 'utils'; import { parseSqsRecord } from 'app/parse-sqs-message'; -import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event'; import { validPdmEvent } from '__tests__/constants'; +import { InvalidEvent } from 'digital-letters-events'; const mockLogger = mock(); +const mockChildLogger = mock(); describe('parseSqsRecord', () => { const messageId = 'test-message-id-123'; @@ -27,6 +28,10 @@ describe('parseSqsRecord', () => { awsRegion: 'eu-west-2', }); + beforeAll(() => { + mockLogger.child.mockReturnValue(mockChildLogger); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -38,13 +43,12 @@ describe('parseSqsRecord', () => { const result = parseSqsRecord(sqsRecord, mockLogger); expect(result).toEqual(validPdmEvent); - expect(mockLogger.info).toHaveBeenCalledWith({ + expect(mockLogger.child).toHaveBeenCalledWith({ messageId }); + expect(mockChildLogger.info).toHaveBeenCalledWith({ description: 'Parsing SQS Record', - messageId, }); - expect(mockLogger.info).toHaveBeenCalledWith({ + expect(mockChildLogger.info).toHaveBeenCalledWith({ description: 'Parsed valid PDMResourceAvailable Event', - messageId, messageReference: validPdmEvent.data.messageReference, senderId: validPdmEvent.data.senderId, resourceId: validPdmEvent.data.resourceId, @@ -53,20 +57,16 @@ describe('parseSqsRecord', () => { }); describe('when SQS record contains an invalid PDMResourceAvailable event', () => { - it('logs error and throws InvalidPdmResourceAvailableEvent', () => { + it('logs error and throws InvalidEvent', () => { const invalidEvent = { ...validPdmEvent, data: {} }; const sqsRecord = createSqsRecord(invalidEvent); - expect(() => parseSqsRecord(sqsRecord, mockLogger)).toThrow( - InvalidPdmResourceAvailableEvent, - ); - - expect(mockLogger.error).toHaveBeenCalledWith( + expect(() => parseSqsRecord(sqsRecord, mockLogger)).toThrow(InvalidEvent); + expect(mockLogger.child).toHaveBeenCalledWith({ messageId }); + expect(mockChildLogger.error).toHaveBeenCalledWith( expect.objectContaining({ - description: - 'The SQS message does not contain a valid PDMResourceAvailable event', - messageId, - error: expect.any(Array), + description: 'Error parsing PDMResourceAvailable event', + err: expect.any(Array), }), ); }); @@ -92,9 +92,8 @@ describe('parseSqsRecord', () => { }; expect(() => parseSqsRecord(sqsRecord, mockLogger)).toThrow(SyntaxError); - expect(mockLogger.info).toHaveBeenCalledWith({ + expect(mockChildLogger.info).toHaveBeenCalledWith({ description: 'Parsing SQS Record', - messageId, }); }); }); diff --git a/lambdas/core-notifier-lambda/src/app/parse-sqs-message.ts b/lambdas/core-notifier-lambda/src/app/parse-sqs-message.ts index ce78a3b06..522fce530 100644 --- a/lambdas/core-notifier-lambda/src/app/parse-sqs-message.ts +++ b/lambdas/core-notifier-lambda/src/app/parse-sqs-message.ts @@ -1,34 +1,26 @@ import type { SQSRecord } from 'aws-lambda'; import { Logger } from 'utils'; -import { PDMResourceAvailable } from 'digital-letters-events'; -import { InvalidPdmResourceAvailableEvent } from 'domain/invalid-pdm-resource-available-event'; -import messagePDMResourceAvailableValidator from 'digital-letters-events/PDMResourceAvailable.js'; +import { + PDMResourceAvailable, + validatePDMResourceAvailable, +} from 'digital-letters-events'; export const parseSqsRecord = ( sqsRecord: SQSRecord, logger: Logger, ): PDMResourceAvailable => { - logger.info({ + const childLogger = logger.child({ messageId: sqsRecord.messageId }); + childLogger.info({ description: 'Parsing SQS Record', - messageId: sqsRecord.messageId, }); const sqsEventBody = JSON.parse(sqsRecord.body); const sqsEventDetail = sqsEventBody.detail; - if (!messagePDMResourceAvailableValidator(sqsEventDetail)) { - logger.error({ - error: messagePDMResourceAvailableValidator.errors, - description: - 'The SQS message does not contain a valid PDMResourceAvailable event', - messageId: sqsRecord.messageId, - }); - throw new InvalidPdmResourceAvailableEvent(sqsRecord.messageId); - } + validatePDMResourceAvailable(sqsEventDetail, childLogger); - logger.info({ + childLogger.info({ description: 'Parsed valid PDMResourceAvailable Event', - messageId: sqsRecord.messageId, messageReference: sqsEventDetail.data.messageReference, senderId: sqsEventDetail.data.senderId, resourceId: sqsEventDetail.data.resourceId, diff --git a/lambdas/core-notifier-lambda/src/domain/invalid-pdm-resource-available-event.ts b/lambdas/core-notifier-lambda/src/domain/invalid-pdm-resource-available-event.ts deleted file mode 100644 index f803a3f9a..000000000 --- a/lambdas/core-notifier-lambda/src/domain/invalid-pdm-resource-available-event.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class InvalidPdmResourceAvailableEvent extends Error { - readonly sqsMessageId: string; - - constructor(sqsMessageId: string) { - super('Unable to parse PDMResourceAvailable event from SQS message'); - this.sqsMessageId = sqsMessageId; - } -} diff --git a/lambdas/file-scanner-lambda/src/__tests__/apis/sqs-handler.test.ts b/lambdas/file-scanner-lambda/src/__tests__/apis/sqs-handler.test.ts index 6fb343295..0f961678e 100644 --- a/lambdas/file-scanner-lambda/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/file-scanner-lambda/src/__tests__/apis/sqs-handler.test.ts @@ -345,7 +345,7 @@ describe('SQS Handler', () => { ); expect(mockLogger.warn).toHaveBeenCalledWith( expect.objectContaining({ - description: 'Error parsing queue entry', + description: 'Error parsing SQS record', }), ); }); @@ -405,7 +405,7 @@ describe('SQS Handler', () => { expect(mockFileScanner.scanFile).not.toHaveBeenCalled(); expect(mockLogger.warn).toHaveBeenCalledWith( expect.objectContaining({ - description: 'Error parsing queue entry', + description: 'Error parsing SQS record', }), ); expect(mockLogger.warn).toHaveBeenCalledWith( diff --git a/lambdas/file-scanner-lambda/src/apis/sqs-handler.ts b/lambdas/file-scanner-lambda/src/apis/sqs-handler.ts index d8b7b7062..7f438f5d3 100644 --- a/lambdas/file-scanner-lambda/src/apis/sqs-handler.ts +++ b/lambdas/file-scanner-lambda/src/apis/sqs-handler.ts @@ -4,8 +4,7 @@ import type { SQSBatchResponse, SQSEvent, } from 'aws-lambda'; -import { ItemDequeued } from 'digital-letters-events'; -import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; +import { ItemDequeued, validateItemDequeued } from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; export interface HandlerDependencies { @@ -27,15 +26,7 @@ function validateRecord( const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; - const isEventValid = itemDequeuedValidator(sqsEventDetail); - if (!isEventValid) { - logger.warn({ - err: itemDequeuedValidator.errors, - description: 'Error parsing queue entry', - }); - - return null; - } + validateItemDequeued(sqsEventDetail, logger); return { messageId, event: sqsEventDetail }; } catch (error) { diff --git a/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts b/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts index 74c1ac350..a2efe5819 100644 --- a/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/pdm-poll-lambda/src/__tests__/apis/sqs-handler.test.ts @@ -295,13 +295,13 @@ describe('SQS Handler', () => { const result = await handler(event); - expect(logger.warn).toHaveBeenCalledWith({ + expect(logger.error).toHaveBeenCalledWith({ err: expect.arrayContaining([ expect.objectContaining({ instancePath: '/source', }), ]), - description: 'Error parsing queue entry', + description: 'Error parsing PDMResourceSubmitted event', }); expect(logger.info).toHaveBeenCalledWith( @@ -322,13 +322,13 @@ describe('SQS Handler', () => { const result = await handler(event); - expect(logger.warn).toHaveBeenCalledWith({ + expect(logger.error).toHaveBeenCalledWith({ err: expect.arrayContaining([ expect.objectContaining({ instancePath: '/source', }), ]), - description: 'Error parsing queue entry', + description: 'Error parsing PDMResourceUnavailable event', }); expect(logger.info).toHaveBeenCalledWith( diff --git a/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts b/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts index 39ae9c720..6ed26d157 100644 --- a/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts +++ b/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts @@ -9,9 +9,10 @@ import { PDMResourceRetriesExceeded, PDMResourceSubmitted, PDMResourceUnavailable, + validatePDMResourceSubmitted, + validatePDMResourceUnavailable, } from 'digital-letters-events'; import pdmResourceAvailableValidator from 'digital-letters-events/PDMResourceAvailable.js'; -import pdmResourceSubmittedValidator from 'digital-letters-events/PDMResourceSubmitted.js'; import pdmResourceUnavailableValidator from 'digital-letters-events/PDMResourceUnavailable.js'; import pdmResourceRetriesExceededValidator from 'digital-letters-events/PDMResourceRetriesExceeded.js'; import { randomUUID } from 'node:crypto'; @@ -43,28 +44,12 @@ function validateRecord( sqsEventDetail.type === 'uk.nhs.notify.digital.letters.pdm.resource.submitted.v1' ) { - const isEventValid = pdmResourceSubmittedValidator(sqsEventDetail); - if (!isEventValid) { - logger.warn({ - err: pdmResourceSubmittedValidator.errors, - description: 'Error parsing queue entry', - }); - - return null; - } + validatePDMResourceSubmitted(sqsEventDetail, logger); return { messageId, event: sqsEventDetail }; } - const isEventValid = pdmResourceUnavailableValidator(sqsEventDetail); - if (!isEventValid) { - logger.warn({ - err: pdmResourceUnavailableValidator.errors, - description: 'Error parsing queue entry', - }); - - return null; - } + validatePDMResourceUnavailable(sqsEventDetail, logger); return { messageId, event: sqsEventDetail }; } catch (error) { diff --git a/lambdas/pdm-uploader-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/pdm-uploader-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts index 0d8df784c..b86563a1f 100644 --- a/lambdas/pdm-uploader-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts +++ b/lambdas/pdm-uploader-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts @@ -222,7 +222,7 @@ describe('sqs-trigger-lambda', () => { expect(mockEventPublisher.sendEvents).not.toHaveBeenCalled(); expect(mockLogger.error).toHaveBeenCalledWith( expect.objectContaining({ - description: 'Error parsing queue entry', + description: 'Error parsing MESHInboxMessageDownloaded event', }), ); }); diff --git a/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts b/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts index 64c66bc0a..e82a18ec7 100644 --- a/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts @@ -9,12 +9,12 @@ import type { UploadToPdmOutcome, UploadToPdmResult, } from 'app/upload-to-pdm'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; import pdmResourceSubmittedValidator from 'digital-letters-events/PDMResourceSubmitted.js'; import pdmResourceSubmissionRejectedValidator from 'digital-letters-events/PDMResourceSubmissionRejected.js'; import { MESHInboxMessageDownloaded, PDMResourceSubmitted, + validateMESHInboxMessageDownloaded, } from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; @@ -42,14 +42,7 @@ function validateRecord( const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; - const isEventValid = messageDownloadedValidator(sqsEventDetail); - if (!isEventValid) { - logger.error({ - err: messageDownloadedValidator.errors, - description: 'Error parsing queue entry', - }); - return null; - } + validateMESHInboxMessageDownloaded(sqsEventDetail, logger); return { messageId, event: sqsEventDetail }; } catch (error) { diff --git a/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts b/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts index 442a53129..c01f79ce4 100644 --- a/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts @@ -6,6 +6,8 @@ import { fileSafeEvent, fivePagePdf, recordEvent } from '__tests__/test-data'; import { FileSafe } from 'digital-letters-events'; const logger = mock(); +const mockChildLogger = mock(); +logger.child.mockReturnValue(mockChildLogger); const eventPublisher = mock(); jest.mock('node:crypto', () => ({ @@ -112,14 +114,13 @@ describe('SQS Handler', () => { const result = await handler(event); - expect(logger.warn).toHaveBeenCalledWith({ + expect(mockChildLogger.error).toHaveBeenCalledWith({ err: expect.arrayContaining([ expect.objectContaining({ instancePath: '/source', }), ]), - description: 'Error parsing print analyser queue entry', - messageReference: invalidFileSafeEvent.data.messageReference, + description: 'Error parsing FileSafe event', }); expect(logger.info).toHaveBeenCalledWith( @@ -137,14 +138,13 @@ describe('SQS Handler', () => { const result = await handler(event); - expect(logger.warn).toHaveBeenCalledWith({ + expect(mockChildLogger.error).toHaveBeenCalledWith({ err: expect.arrayContaining([ expect.objectContaining({ message: `must have required property 'specversion'`, }), ]), - description: 'Error parsing print analyser queue entry', - messageReference: 'not present', + description: 'Error parsing FileSafe event', }); expect(logger.info).toHaveBeenCalledWith( diff --git a/lambdas/print-analyser/src/apis/sqs-handler.ts b/lambdas/print-analyser/src/apis/sqs-handler.ts index 54bf51502..1f20bd3b3 100644 --- a/lambdas/print-analyser/src/apis/sqs-handler.ts +++ b/lambdas/print-analyser/src/apis/sqs-handler.ts @@ -5,8 +5,11 @@ import type { } from 'aws-lambda'; import { createHash, randomUUID } from 'node:crypto'; import { PDFDocument } from 'pdf-lib'; -import { FileSafe, PDFAnalysed } from 'digital-letters-events'; -import fileSafeValidator from 'digital-letters-events/FileSafe.js'; +import { + FileSafe, + PDFAnalysed, + validateFileSafe, +} from 'digital-letters-events'; import pdfAnalysedValidator from 'digital-letters-events/PDFAnalysed.js'; import { EventPublisher, Logger, getS3ObjectBufferFromUri } from 'utils'; @@ -33,17 +36,11 @@ function validateRecord( const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; - const isEventValid = fileSafeValidator(sqsEventDetail); - if (!isEventValid) { - logger.warn({ - err: fileSafeValidator.errors, - description: 'Error parsing print analyser queue entry', - messageReference: - sqsEventDetail?.data?.messageReference || 'not present', - }); + const messageReference = + sqsEventDetail?.data?.messageReference || 'not present'; + const childLogger = logger.child({ messageReference }); - return null; - } + validateFileSafe(sqsEventDetail, childLogger); return { messageId, event: sqsEventDetail }; } catch (error) { diff --git a/lambdas/print-sender-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/print-sender-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts index d67793df7..c0d17cd8a 100644 --- a/lambdas/print-sender-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts +++ b/lambdas/print-sender-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts @@ -49,6 +49,7 @@ const createValidEvent = (overrides = {}): PDFAnalysed => ({ describe('sqs-trigger-lambda', () => { let mockPrintSender: jest.Mocked; let mockLogger: jest.Mocked; + let mockChildLogger: jest.Mocked; let handler: any; beforeEach(() => { @@ -56,10 +57,17 @@ describe('sqs-trigger-lambda', () => { send: jest.fn(), } as unknown as jest.Mocked; + mockChildLogger = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + } as unknown as jest.Mocked; + mockLogger = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), + child: jest.fn().mockReturnValue(mockChildLogger), } as unknown as jest.Mocked; handler = createHandler({ @@ -110,10 +118,9 @@ describe('sqs-trigger-lambda', () => { const result = await handler(sqsEvent); expect(result.batchItemFailures).toEqual([{ itemIdentifier: 'message-1' }]); - expect(mockLogger.error).toHaveBeenCalledWith( + expect(mockChildLogger.error).toHaveBeenCalledWith( expect.objectContaining({ - description: 'Error parsing print sender queue entry', - messageReference: 'not present', + description: 'Error parsing PDFAnalysed event', }), ); expect(mockPrintSender.send).not.toHaveBeenCalled(); diff --git a/lambdas/print-sender-lambda/src/apis/sqs-trigger-lambda.ts b/lambdas/print-sender-lambda/src/apis/sqs-trigger-lambda.ts index a9936123b..f06751e65 100644 --- a/lambdas/print-sender-lambda/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/print-sender-lambda/src/apis/sqs-trigger-lambda.ts @@ -5,8 +5,7 @@ import type { } from 'aws-lambda'; import type { PrintSender, PrintSenderOutcome } from 'app/print-sender'; import { Logger } from 'utils'; -import pdfAnalysedValidator from 'digital-letters-events/PDFAnalysed.js'; -import { PDFAnalysed } from 'digital-letters-events'; +import { validatePDFAnalysed } from 'digital-letters-events'; interface PrintSenderHandlerDependencies { printSender: PrintSender; @@ -27,20 +26,13 @@ export const createHandler = ({ const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; - const isEventValid = pdfAnalysedValidator(sqsEventDetail); - if (!isEventValid) { - logger.error({ - err: pdfAnalysedValidator.errors, - description: 'Error parsing print sender queue entry', - messageReference: - sqsEventDetail?.data?.messageReference || 'not present', - }); - batchItemFailures.push({ itemIdentifier: messageId }); - return; - } - const pdfAnalysedEvent: PDFAnalysed = sqsEventDetail; + const messageReference = + sqsEventDetail?.data?.messageReference || 'not present'; + const childLogger = logger.child({ messageReference }); + + validatePDFAnalysed(sqsEventDetail, childLogger); - const result = await printSender.send(pdfAnalysedEvent); + const result = await printSender.send(sqsEventDetail); if (result === 'failed') { batchItemFailures.push({ itemIdentifier: messageId }); diff --git a/lambdas/report-generator/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/report-generator/src/__tests__/apis/sqs-trigger-lambda.test.ts index b0eee1767..7cb243548 100644 --- a/lambdas/report-generator/src/__tests__/apis/sqs-trigger-lambda.test.ts +++ b/lambdas/report-generator/src/__tests__/apis/sqs-trigger-lambda.test.ts @@ -200,7 +200,7 @@ describe('sqs-trigger-lambda', () => { expect(response.batchItemFailures).toEqual([{ itemIdentifier: 'msg-1' }]); expect(mockLogger.error).toHaveBeenCalledWith( expect.objectContaining({ - description: 'Error parsing queue entry', + description: 'Error parsing GenerateReport event', }), ); expect(mockReportGenerator.generate).not.toHaveBeenCalled(); diff --git a/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts b/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts index 61536adf1..755ee5a24 100644 --- a/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts @@ -9,9 +9,12 @@ import type { ReportGeneratorOutcome, ReportGeneratorResult, } from 'app/report-generator'; -import generateReportValidator from 'digital-letters-events/GenerateReport.js'; import reportGeneratedValidator from 'digital-letters-events/ReportGenerated.js'; -import { GenerateReport, ReportGenerated } from 'digital-letters-events'; +import { + GenerateReport, + ReportGenerated, + validateGenerateReport, +} from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; interface ProcessingResult { @@ -38,14 +41,7 @@ function validateRecord( const sqsEventBody = JSON.parse(body); const sqsEventDetail = sqsEventBody.detail; - const isEventValid = generateReportValidator(sqsEventDetail); - if (!isEventValid) { - logger.error({ - err: generateReportValidator.errors, - description: 'Error parsing queue entry', - }); - return null; - } + validateGenerateReport(sqsEventDetail, logger); return { messageId, event: sqsEventDetail }; } catch (error) { diff --git a/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts b/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts index 1f1fc1afe..bb68c78e2 100644 --- a/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts @@ -36,24 +36,17 @@ export const createHandler = ({ async ({ body, messageId }): Promise => { try { const sqsEventBody = JSON.parse(body); - const messageDownloadedEvent = validateMESHInboxMessageDownloaded( - sqsEventBody.detail, - logger, - ); + const sqsEventDetail = sqsEventBody.detail; + validateMESHInboxMessageDownloaded(sqsEventDetail, logger); - if (!messageDownloadedEvent) { - batchItemFailures.push({ itemIdentifier: messageId }); - return { result: 'failed' }; - } - - const result = await createTtl.send(messageDownloadedEvent); + const result = await createTtl.send(sqsEventDetail); if (result === 'failed') { batchItemFailures.push({ itemIdentifier: messageId }); return { result: 'failed' }; } - return { result, item: messageDownloadedEvent }; + return { result, item: sqsEventDetail }; } catch (error) { logger.error({ err: error, diff --git a/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts b/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts index 7ef9c53c6..208498b97 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts @@ -247,10 +247,10 @@ describe('createHandler', () => { const result = await handler(mockInvalidEvent); - expect(logger.warn).toHaveBeenCalledWith( + expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ err: expect.any(Object), - description: 'Error parsing ttl item event', + description: 'Error parsing MESHInboxMessageDownloaded event', }), ); diff --git a/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts b/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts index a3ba0f7bb..b5c204bc8 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts @@ -5,12 +5,11 @@ import type { DynamoDBRecord, DynamoDBStreamEvent, } from 'aws-lambda'; -import type { +import { ItemDequeued, - MESHInboxMessageDownloaded, + validateMESHInboxMessageDownloaded, } from 'digital-letters-events'; import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; import { randomUUID } from 'node:crypto'; import { $TtlDynamodbRecord, EventPublisher, Logger } from 'utils'; @@ -20,10 +19,6 @@ export type CreateHandlerDependencies = { logger: Logger; }; -const eventValidator = messageDownloadedValidator as ( - d: unknown, -) => d is MESHInboxMessageDownloaded; - export const createHandler = ({ dlq, eventPublisher, @@ -68,19 +63,8 @@ export const createHandler = ({ return; } - let itemEvent: MESHInboxMessageDownloaded; - if (eventValidator(item.event)) { - itemEvent = item.event; - } else { - logger.warn({ - err: messageDownloadedValidator.errors, - description: 'Error parsing ttl item event', - }); - - failures.push(record); - - return; - } + const itemEvent = item.event; + validateMESHInboxMessageDownloaded(itemEvent, logger); if (item.withdrawn) { logger.info({ diff --git a/src/cloudevents/domains/digital-letters/2025-10-draft/defs/print.schema.yaml b/src/cloudevents/domains/digital-letters/2025-10-draft/defs/print.schema.yaml index 2ced7c8f7..2a1b80232 100644 --- a/src/cloudevents/domains/digital-letters/2025-10-draft/defs/print.schema.yaml +++ b/src/cloudevents/domains/digital-letters/2025-10-draft/defs/print.schema.yaml @@ -52,7 +52,6 @@ properties: examples: - "s3://my-bucket/path/to/my-object" createdAt: - title: "Created At DateTime" description: "Timestamp when the letter was created (RFC 3339)." examples: [ "2025-10-01T10:15:30.000Z" diff --git a/src/digital-letters-events/errors/InvalidEvent.ts b/src/digital-letters-events/errors/InvalidEvent.ts new file mode 100644 index 000000000..597c2e3c0 --- /dev/null +++ b/src/digital-letters-events/errors/InvalidEvent.ts @@ -0,0 +1,8 @@ +export class InvalidEvent extends Error { + readonly errors: any; + + constructor(errors: any) { + super('Unable to parse event'); + this.errors = errors; + } +} diff --git a/src/digital-letters-events/errors/index.ts b/src/digital-letters-events/errors/index.ts new file mode 100644 index 000000000..efe5bb906 --- /dev/null +++ b/src/digital-letters-events/errors/index.ts @@ -0,0 +1 @@ +export * from './InvalidEvent'; diff --git a/src/digital-letters-events/index.ts b/src/digital-letters-events/index.ts index 804f8e484..c79fa396c 100644 --- a/src/digital-letters-events/index.ts +++ b/src/digital-letters-events/index.ts @@ -1,2 +1,3 @@ export * from './types'; export * from './guard-functions'; +export * from './errors'; diff --git a/src/typescript-schema-generator/package.json b/src/typescript-schema-generator/package.json index dc988426c..77a841fb1 100644 --- a/src/typescript-schema-generator/package.json +++ b/src/typescript-schema-generator/package.json @@ -18,7 +18,8 @@ "name": "typescript-schema-generator", "private": true, "scripts": { - "generate-dependencies": "npm run generate-types && npm run generate-validators && npm run generate-guard-functions", + "clean": "rm -rf ../digital-letters-events/guard-functions ../digital-letters-events/validators ../digital-letters-events/types", + "generate-dependencies": "npm run clean && npm run generate-types && npm run generate-validators && npm run generate-guard-functions", "generate-guard-functions": "tsx src/generate-guard-functions-cli.ts", "generate-types": "tsx src/generate-types-cli.ts", "generate-validators": "tsx src/generate-validators-cli.ts", diff --git a/src/typescript-schema-generator/src/generate-guard-functions.ts b/src/typescript-schema-generator/src/generate-guard-functions.ts index 246ab0e46..453dda48f 100644 --- a/src/typescript-schema-generator/src/generate-guard-functions.ts +++ b/src/typescript-schema-generator/src/generate-guard-functions.ts @@ -19,26 +19,21 @@ export async function generateGuardFunctions() { const validatorVariableName = `event${typeName}Validator`; let guardFunction = `import ${validatorVariableName} from 'digital-letters-events/${typeName}.js'\n`; - guardFunction += `import type { ${typeName} } from 'digital-letters-events';\n`; + guardFunction += `import { InvalidEvent, type ${typeName} } from 'digital-letters-events';`; guardFunction += `import { Logger } from 'utils';\n\n`; guardFunction += `export function validate${typeName}(\n`; guardFunction += ` event: unknown,\n`; guardFunction += ` logger: Logger,\n`; - guardFunction += `): ${typeName} | null {\n\n`; - - guardFunction += ` const eventValidator = event${typeName}Validator as (d: unknown) => d is ${typeName};\n\n`; - - guardFunction += ` if (!eventValidator(event)) {\n`; + guardFunction += `): asserts event is ${typeName} {\n`; + guardFunction += ` if (!${validatorVariableName}(event)) {\n`; guardFunction += ` logger.error({\n`; guardFunction += ` err: ${validatorVariableName}.errors,\n`; guardFunction += ` description: 'Error parsing ${typeName} event',\n`; guardFunction += ` });\n`; - guardFunction += ` return null;\n`; - guardFunction += ` }\n\n`; - - guardFunction += ` return event;\n`; - guardFunction += `}\n\n`; + guardFunction += ` throw new InvalidEvent(${validatorVariableName}.errors);\n`; + guardFunction += ` }\n`; + guardFunction += `}\n`; const typeDeclarationName = `${typeName}`; diff --git a/utils/utils/src/logger.ts b/utils/utils/src/logger.ts index d1c1a9106..ede959958 100644 --- a/utils/utils/src/logger.ts +++ b/utils/utils/src/logger.ts @@ -4,7 +4,10 @@ const { combine, errors, json, timestamp } = winston.format; export const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', - format: combine(timestamp(), json(), errors({ stack: true, cause: true })), + format: combine( + errors({ stack: true, cause: true }), + timestamp(), + json()), transports: [ new winston.transports.Stream({ stream: process.stdout, From c1ddcf809fcd5efa2484c6cd969643ab0c5ea651 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Tue, 24 Mar 2026 16:11:50 +0000 Subject: [PATCH 03/17] CCM-13675: Event code generation improvements --- .../jest.config.ts | 1 + .../generate-guard-functions.test.ts | 61 +++++++++++++++++++ .../src/__tests__/generate-types.test.ts | 9 +-- .../src/generate-guard-functions-cli.ts | 2 +- .../src/generate-guard-functions.ts | 3 +- .../src/generate-types.ts | 2 +- 6 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts diff --git a/src/typescript-schema-generator/jest.config.ts b/src/typescript-schema-generator/jest.config.ts index a02be289f..8bc05484c 100644 --- a/src/typescript-schema-generator/jest.config.ts +++ b/src/typescript-schema-generator/jest.config.ts @@ -4,6 +4,7 @@ const config = { ...baseJestConfig, coveragePathIgnorePatterns: [ ...(baseJestConfig.coveragePathIgnorePatterns ?? []), + 'generate-guard-functions-cli.ts', 'src/generate-types-cli.ts', 'src/generate-validators-cli.ts', ], diff --git a/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts b/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts new file mode 100644 index 000000000..fb9da4b7a --- /dev/null +++ b/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts @@ -0,0 +1,61 @@ +/* eslint-disable security/detect-non-literal-fs-filename */ + +import { destinationPackageName } from 'file-utils'; +import { generateGuardFunctions } from 'generate-guard-functions'; +import mockFs from 'mock-fs'; +import { readFileSync, readdirSync } from 'node:fs'; +import path from 'node:path'; +import { eventSchemasDir } from 'utils'; + +jest.mock('json-schema-to-typescript'); + +describe('generate-guard-functions', () => { + const outputDir = path.resolve( + __dirname, + '..', + '..', + '..', + destinationPackageName, + 'guard-functions', + ); + + beforeEach(() => { + mockFs({ + [eventSchemasDir]: { + 'one.flattened.schema.json': '{"title": "One"}', + 'two.flattened.schema.json': '{"title": "Two"}', + 'three.flattened.schema.json': '{"title": "Three"}', + }, + }); + + jest.spyOn(console, 'log').mockImplementation(() => {}); + jest.spyOn(console, 'group').mockImplementation(() => {}); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should generate a guard function file for each schema', async () => { + await generateGuardFunctions(); + + const typeDeclarationFiles = readdirSync(outputDir); + + expect(typeDeclarationFiles.length).toBe(4); + expect(typeDeclarationFiles).toEqual( + expect.arrayContaining(['index.ts', 'One.ts', 'Two.ts', 'Three.ts']), + ); + }); + + it('should create an index file exporting all generated guard function', async () => { + await generateGuardFunctions(); + + const indexFileContents = readFileSync( + path.join(outputDir, 'index.ts'), + 'utf8', + ); + expect(indexFileContents).toContain("export * from './One';"); + expect(indexFileContents).toContain("export * from './Two';"); + expect(indexFileContents).toContain("export * from './Three';"); + }); +}); diff --git a/src/typescript-schema-generator/src/__tests__/generate-types.test.ts b/src/typescript-schema-generator/src/__tests__/generate-types.test.ts index 153fc005f..918e07604 100644 --- a/src/typescript-schema-generator/src/__tests__/generate-types.test.ts +++ b/src/typescript-schema-generator/src/__tests__/generate-types.test.ts @@ -46,12 +46,7 @@ describe('generate-types', () => { expect(typeDeclarationFiles.length).toBe(4); expect(typeDeclarationFiles).toEqual( - expect.arrayContaining([ - 'index.d.ts', - 'One.d.ts', - 'Two.d.ts', - 'Three.d.ts', - ]), + expect.arrayContaining(['index.ts', 'One.ts', 'Two.ts', 'Three.ts']), ); }); @@ -59,7 +54,7 @@ describe('generate-types', () => { await generateTypes(); const indexFileContents = readFileSync( - path.join(outputDir, 'index.d.ts'), + path.join(outputDir, 'index.ts'), 'utf8', ); expect(indexFileContents).toContain("export * from './One';"); diff --git a/src/typescript-schema-generator/src/generate-guard-functions-cli.ts b/src/typescript-schema-generator/src/generate-guard-functions-cli.ts index 454b7555c..692d3d925 100644 --- a/src/typescript-schema-generator/src/generate-guard-functions-cli.ts +++ b/src/typescript-schema-generator/src/generate-guard-functions-cli.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ -import { generateGuardFunctions } from 'generate-guard-functions' +import { generateGuardFunctions } from 'generate-guard-functions'; generateGuardFunctions().catch((error) => { console.error('Error generating guard functions:', error); diff --git a/src/typescript-schema-generator/src/generate-guard-functions.ts b/src/typescript-schema-generator/src/generate-guard-functions.ts index 453dda48f..6f4e57502 100644 --- a/src/typescript-schema-generator/src/generate-guard-functions.ts +++ b/src/typescript-schema-generator/src/generate-guard-functions.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { createOutputDir, writeFile, writeTypesIndex } from 'file-utils'; +import { createOutputDir, writeFile } from 'file-utils'; import path from 'node:path'; import { writeFileSync } from 'node:fs'; import { eventSchemasDir, listEventSchemas, loadSchema } from 'utils'; @@ -35,7 +35,6 @@ export async function generateGuardFunctions() { guardFunction += ` }\n`; guardFunction += `}\n`; - const typeDeclarationName = `${typeName}`; const typeDeclarationFilename = `${typeDeclarationName}.ts`; writeFile(outputDir, typeDeclarationFilename, guardFunction); diff --git a/src/typescript-schema-generator/src/generate-types.ts b/src/typescript-schema-generator/src/generate-types.ts index 6a38b3897..d6384b839 100644 --- a/src/typescript-schema-generator/src/generate-types.ts +++ b/src/typescript-schema-generator/src/generate-types.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import { createOutputDir, writeFile, writeTypesIndex } from 'file-utils'; +import { createOutputDir, writeFile } from 'file-utils'; import { compile } from 'json-schema-to-typescript'; import { writeFileSync } from 'node:fs'; import path from 'node:path'; From cdc610479dd3e6c82ceb1de891757954db114ad0 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Tue, 24 Mar 2026 16:17:46 +0000 Subject: [PATCH 04/17] CCM-13675: Event code generation improvements --- utils/utils/src/logger.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/utils/utils/src/logger.ts b/utils/utils/src/logger.ts index ede959958..894fcba4b 100644 --- a/utils/utils/src/logger.ts +++ b/utils/utils/src/logger.ts @@ -4,10 +4,7 @@ const { combine, errors, json, timestamp } = winston.format; export const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', - format: combine( - errors({ stack: true, cause: true }), - timestamp(), - json()), + format: combine(errors({ stack: true, cause: true }), timestamp(), json()), transports: [ new winston.transports.Stream({ stream: process.stdout, From eeeb3305459f1da6b9f8dccd24bea9f8a459ac4e Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Tue, 24 Mar 2026 17:12:45 +0000 Subject: [PATCH 05/17] CCM-13675: Fix component tests --- .../print-analyser.component.spec.ts | 2 +- .../print-sender.component.spec.ts | 2 +- .../ttl-create.component.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts index 7638fe3bb..070b86ed6 100644 --- a/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts @@ -104,7 +104,7 @@ test.describe('Print analyser', () => { const eventLogEntry = await getLogsFromCloudwatch( PRINT_ANALYSER_LAMBDA_LOG_GROUP_NAME, [ - '$.message.description = "Error parsing print analyser queue entry"', + '$.message.description = "Error parsing FileSafe event"', `$.message.err[0].message = "must have required property 'senderId'"`, `$.message.messageReference = "${messageReference}"`, ], diff --git a/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts index 4afa53674..5dad416d7 100644 --- a/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts @@ -103,7 +103,7 @@ test.describe('Digital Letters - Print Sender', () => { const eventLogEntry = await getLogsFromCloudwatch( PRINT_SENDER_LAMBDA_LOG_GROUP_NAME, [ - '$.message.description = "Error parsing print sender queue entry"', + '$.message.description = "Error parsing PDFAnalysed event"', `$.message.err[0].message = "must have required property 'senderId'"`, `$.message.messageReference = "${messageReference}"`, ], diff --git a/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts b/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts index 42d626ef2..5afee577d 100644 --- a/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts @@ -154,7 +154,7 @@ test.describe('Digital Letters - Create TTL', () => { const eventLogEntry = await getLogsFromCloudwatch( CREATE_TTL_LAMBDA_LOG_GROUP_NAME, [ - '$.message.description = "Error parsing ttl queue entry"', + '$.message.description = "Error parsing MESHInboxMessageDownloaded event"', `$.message.err[0].params.additionalProperty = "${unexpectedField}"`, ], ); From 17763d6003dfcaf3c7bc4383f4c17c0a4c642d04 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Wed, 25 Mar 2026 08:45:55 +0000 Subject: [PATCH 06/17] CCM-13675: Fix component tests --- .../print-analyser.component.spec.ts | 2 +- .../print-sender.component.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts index 070b86ed6..b71ad2101 100644 --- a/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts @@ -106,7 +106,7 @@ test.describe('Print analyser', () => { [ '$.message.description = "Error parsing FileSafe event"', `$.message.err[0].message = "must have required property 'senderId'"`, - `$.message.messageReference = "${messageReference}"`, + `$.messageReference = "${messageReference}"`, ], ); diff --git a/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts index 5dad416d7..0988c5a63 100644 --- a/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts @@ -105,7 +105,7 @@ test.describe('Digital Letters - Print Sender', () => { [ '$.message.description = "Error parsing PDFAnalysed event"', `$.message.err[0].message = "must have required property 'senderId'"`, - `$.message.messageReference = "${messageReference}"`, + `$.messageReference = "${messageReference}"`, ], ); From 55038483f8c7d399b4d65834948a7aab0f054e82 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Wed, 25 Mar 2026 14:55:39 +0000 Subject: [PATCH 07/17] CCM-13675: Get rid of js validators in lamdbas --- .../src/apis/sqs-handler.ts | 12 ++++---- .../src/__tests__/domain/mapper.test.ts | 25 ++++++++--------- .../src/apis/sqs-handler.ts | 16 +++++------ .../pdm-poll-lambda/src/apis/sqs-handler.ts | 10 +++---- .../src/apis/sqs-trigger-lambda.ts | 8 +++--- .../print-analyser/src/apis/sqs-handler.ts | 4 +-- .../src/apis/sqs-handler.ts | 5 ++-- .../src/apis/sqs-trigger-lambda.ts | 4 +-- .../apis/scheduled-event-handler.test.ts | 17 +++++++---- .../src/apis/scheduled-event-handler.ts | 5 ++-- .../__tests__/apis/sqs-trigger-lambda.test.ts | 17 +++++------ .../src/apis/sqs-trigger-lambda.ts | 4 +-- .../apis/dynamodb-stream-handler.test.ts | 10 ++++--- .../src/apis/dynamodb-stream-handler.ts | 4 +-- .../event-publisher/event-publisher.test.ts | 28 +++++++++++-------- .../src/event-publisher/event-publisher.ts | 13 +++------ 16 files changed, 92 insertions(+), 90 deletions(-) diff --git a/lambdas/core-notifier-lambda/src/apis/sqs-handler.ts b/lambdas/core-notifier-lambda/src/apis/sqs-handler.ts index eb217bab0..1a87ad774 100644 --- a/lambdas/core-notifier-lambda/src/apis/sqs-handler.ts +++ b/lambdas/core-notifier-lambda/src/apis/sqs-handler.ts @@ -10,6 +10,9 @@ import { MessageRequestSkipped, MessageRequestSubmitted, PDMResourceAvailable, + validateMessageRequestRejected, + validateMessageRequestSkipped, + validateMessageRequestSubmitted, } from 'digital-letters-events'; import { mapPdmEventToMessageRequestRejected, @@ -17,9 +20,6 @@ import { mapPdmEventToMessageRequestSubmitted, mapPdmEventToSingleMessageRequest, } from 'domain/mapper'; -import messageRequestSubmittedValidator from 'digital-letters-events/MessageRequestSubmitted.js'; -import messageRequestRejectedValidator from 'digital-letters-events/MessageRequestRejected.js'; -import messageRequestSkippedValidator from 'digital-letters-events/MessageRequestSkipped.js'; import { parseSqsRecord } from 'app/parse-sqs-message'; import type { NotifyMessageProcessor } from 'app/notify-message-processor'; @@ -185,17 +185,17 @@ export const createHandler = ({ submittedEvents.length > 0 && eventPublisher.sendEvents( submittedEvents, - messageRequestSubmittedValidator, + validateMessageRequestSubmitted, ), skippedEvents.length > 0 && eventPublisher.sendEvents( skippedEvents, - messageRequestSkippedValidator, + validateMessageRequestSkipped, ), rejectedEvents.length > 0 && eventPublisher.sendEvents( rejectedEvents, - messageRequestRejectedValidator, + validateMessageRequestRejected, ), ].filter(Boolean), ); diff --git a/lambdas/move-scanned-files-lambda/src/__tests__/domain/mapper.test.ts b/lambdas/move-scanned-files-lambda/src/__tests__/domain/mapper.test.ts index f92f1ee2c..8c1200554 100644 --- a/lambdas/move-scanned-files-lambda/src/__tests__/domain/mapper.test.ts +++ b/lambdas/move-scanned-files-lambda/src/__tests__/domain/mapper.test.ts @@ -1,6 +1,10 @@ +import { + validateFileQuarantined, + validateFileSafe, +} from 'digital-letters-events'; import { createFileQuarantinedEvent, createFileSafeEvent } from 'domain/mapper'; -import fileSafeValidator from 'digital-letters-events/FileSafe.js'; -import fileQuarantinedValidator from 'digital-letters-events/FileQuarantined.js'; +import { mock } from 'jest-mock-extended'; +import { Logger } from 'utils'; // Mock randomUUID to make tests deterministic jest.mock('node:crypto', () => ({ @@ -22,6 +26,7 @@ describe('mapper', () => { }); describe('createFileSafeEvent', () => { + const mockLogger = mock(); it('creates a FileSafe event with correct structure', () => { const messageReference = 'msg-ref-123'; const senderId = 'sender-456'; @@ -52,11 +57,7 @@ describe('mapper', () => { recordedtime: '2024-01-15T10:30:00.000Z', severitynumber: 2, }); - const isValid = fileSafeValidator(result); - if (!isValid) { - throw new Error(JSON.stringify(fileSafeValidator.errors, null, 2)); - } - expect(isValid).toBe(true); + expect(() => validateFileSafe(result, mockLogger)).not.toThrow(); }); it('handles different input values correctly', () => { @@ -80,6 +81,8 @@ describe('mapper', () => { }); describe('createFileQuarantinedEvent', () => { + const mockLogger = mock(); + it('creates a FileQuarantined event with correct structure', () => { const messageReference = 'msg-ref-789'; const senderId = 'sender-012'; @@ -110,13 +113,7 @@ describe('mapper', () => { recordedtime: '2024-01-15T10:30:00.000Z', severitynumber: 2, }); - const isValid = fileQuarantinedValidator(result); - if (!isValid) { - throw new Error( - JSON.stringify(fileQuarantinedValidator.errors, null, 2), - ); - } - expect(isValid).toBe(true); + expect(() => validateFileQuarantined(result, mockLogger)).not.toThrow(); }); }); }); diff --git a/lambdas/move-scanned-files-lambda/src/apis/sqs-handler.ts b/lambdas/move-scanned-files-lambda/src/apis/sqs-handler.ts index f142e35f6..11a2b9399 100644 --- a/lambdas/move-scanned-files-lambda/src/apis/sqs-handler.ts +++ b/lambdas/move-scanned-files-lambda/src/apis/sqs-handler.ts @@ -6,9 +6,12 @@ import type { SQSRecord, } from 'aws-lambda'; import { EventPublisher, Logger } from 'utils'; -import { FileQuarantined, FileSafe } from 'digital-letters-events'; -import fileSafeValidator from 'digital-letters-events/FileSafe.js'; -import fileQuarantinedValidator from 'digital-letters-events/FileQuarantined.js'; +import { + FileQuarantined, + FileSafe, + validateFileQuarantined, + validateFileSafe, +} from 'digital-letters-events'; import { parseSqsRecord } from 'app/parse-sqs-message'; import { MoveFileHandler } from 'app/move-file-handler'; @@ -69,14 +72,11 @@ export const createHandler = ({ await Promise.all( [ fileSafeEvents.length > 0 && - eventPublisher.sendEvents( - fileSafeEvents, - fileSafeValidator, - ), + eventPublisher.sendEvents(fileSafeEvents, validateFileSafe), fileQuarantinedEvents.length > 0 && eventPublisher.sendEvents( fileQuarantinedEvents, - fileQuarantinedValidator, + validateFileQuarantined, ), ].filter(Boolean), ); diff --git a/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts b/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts index 6ed26d157..44c8e9fe0 100644 --- a/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts +++ b/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts @@ -9,12 +9,10 @@ import { PDMResourceRetriesExceeded, PDMResourceSubmitted, PDMResourceUnavailable, + validatePDMResourceRetriesExceeded, validatePDMResourceSubmitted, validatePDMResourceUnavailable, } from 'digital-letters-events'; -import pdmResourceAvailableValidator from 'digital-letters-events/PDMResourceAvailable.js'; -import pdmResourceUnavailableValidator from 'digital-letters-events/PDMResourceUnavailable.js'; -import pdmResourceRetriesExceededValidator from 'digital-letters-events/PDMResourceRetriesExceeded.js'; import { randomUUID } from 'node:crypto'; import { EventPublisher, Logger } from 'utils'; @@ -195,17 +193,17 @@ export const createHandler = ({ availableEvents.length > 0 && eventPublisher.sendEvents( availableEvents, - pdmResourceAvailableValidator, + validatePDMResourceUnavailable, ), unavailableEvents.length > 0 && eventPublisher.sendEvents( unavailableEvents, - pdmResourceUnavailableValidator, + validatePDMResourceUnavailable, ), retriesExceededEvents.length > 0 && eventPublisher.sendEvents( retriesExceededEvents, - pdmResourceRetriesExceededValidator, + validatePDMResourceRetriesExceeded, ), ].filter(Boolean), ); diff --git a/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts b/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts index e82a18ec7..f3bd6c026 100644 --- a/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/pdm-uploader-lambda/src/apis/sqs-trigger-lambda.ts @@ -9,12 +9,12 @@ import type { UploadToPdmOutcome, UploadToPdmResult, } from 'app/upload-to-pdm'; -import pdmResourceSubmittedValidator from 'digital-letters-events/PDMResourceSubmitted.js'; -import pdmResourceSubmissionRejectedValidator from 'digital-letters-events/PDMResourceSubmissionRejected.js'; import { MESHInboxMessageDownloaded, PDMResourceSubmitted, validateMESHInboxMessageDownloaded, + validatePDMResourceSubmissionRejected, + validatePDMResourceSubmitted, } from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; @@ -150,7 +150,7 @@ async function publishSuccessfulEvents( resourceId, }, })), - pdmResourceSubmittedValidator, + validatePDMResourceSubmitted, ); if (submittedFailedEvents.length > 0) { logger.warn({ @@ -191,7 +191,7 @@ async function publishFailedEvents( senderId: event.data.senderId, }, })), - pdmResourceSubmissionRejectedValidator, + validatePDMResourceSubmissionRejected, ); if (rejectedFailedEvents.length > 0) { logger.warn({ diff --git a/lambdas/print-analyser/src/apis/sqs-handler.ts b/lambdas/print-analyser/src/apis/sqs-handler.ts index 1f20bd3b3..5299a2645 100644 --- a/lambdas/print-analyser/src/apis/sqs-handler.ts +++ b/lambdas/print-analyser/src/apis/sqs-handler.ts @@ -9,8 +9,8 @@ import { FileSafe, PDFAnalysed, validateFileSafe, + validatePDFAnalysed, } from 'digital-letters-events'; -import pdfAnalysedValidator from 'digital-letters-events/PDFAnalysed.js'; import { EventPublisher, Logger, getS3ObjectBufferFromUri } from 'utils'; export interface HandlerDependencies { @@ -130,7 +130,7 @@ export const createHandler = ({ }), ); - await eventPublisher.sendEvents(validEvents, pdfAnalysedValidator); + await eventPublisher.sendEvents(validEvents, validatePDFAnalysed); const processedItemCount = receivedItemCount - batchItemFailures.length; logger.info( diff --git a/lambdas/print-status-handler/src/apis/sqs-handler.ts b/lambdas/print-status-handler/src/apis/sqs-handler.ts index 47a15ed3f..dee78a0cb 100644 --- a/lambdas/print-status-handler/src/apis/sqs-handler.ts +++ b/lambdas/print-status-handler/src/apis/sqs-handler.ts @@ -9,8 +9,7 @@ import { $LetterEvent, LetterEvent, } from '@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events'; -import { PrintLetterTransitioned } from 'digital-letters-events'; -import printLetterTransitionedValidator from 'digital-letters-events/PrintLetterTransitioned.js'; +import { PrintLetterTransitioned, validatePrintLetterTransitioned } from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; export interface HandlerDependencies { @@ -157,7 +156,7 @@ export const createHandler = ({ await eventPublisher.sendEvents( validEvents, - printLetterTransitionedValidator, + validatePrintLetterTransitioned, ); const processedItemCount = receivedItemCount - batchItemFailures.length; diff --git a/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts b/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts index 755ee5a24..14c321f96 100644 --- a/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts @@ -9,11 +9,11 @@ import type { ReportGeneratorOutcome, ReportGeneratorResult, } from 'app/report-generator'; -import reportGeneratedValidator from 'digital-letters-events/ReportGenerated.js'; import { GenerateReport, ReportGenerated, validateGenerateReport, + validateReportGenerated, } from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; @@ -145,7 +145,7 @@ async function publishSuccessfulEvents( const submittedFailedEvents = await eventPublisher.sendEvents( reportGeneratedEvents, - reportGeneratedValidator, + validateReportGenerated, ); return submittedFailedEvents; diff --git a/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts b/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts index 5788adeef..fe8cd4fa0 100644 --- a/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts +++ b/lambdas/report-scheduler/src/__tests__/apis/scheduled-event-handler.test.ts @@ -1,12 +1,12 @@ -import { EventPublisher, Sender } from 'utils'; +import { EventPublisher, Logger, Sender } from 'utils'; import { ISenderManagement } from 'sender-management'; -import { GenerateReport } from 'digital-letters-events'; +import { GenerateReport, validateGenerateReport } from 'digital-letters-events'; import { createHandler } from 'apis/scheduled-event-handler'; -import GenerateReportValidator from 'digital-letters-events/GenerateReport.js'; describe('scheduled-event-handler', () => { let mockSenderManagement: jest.Mocked; let mockEventPublisher: jest.Mocked; + let mockLogger: jest.Mocked; beforeEach(() => { mockSenderManagement = { @@ -17,6 +17,13 @@ describe('scheduled-event-handler', () => { sendEvents: jest.fn(), } as unknown as jest.Mocked; + mockLogger = { + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + child: jest.fn().mockReturnThis(), + } as unknown as jest.Mocked; + jest.useFakeTimers(); }); @@ -101,9 +108,7 @@ describe('scheduled-event-handler', () => { expect(event.time).toBe('2024-01-15T12:00:00.000Z'); expect(event.severitynumber).toBe(2); - const isEventValid = GenerateReportValidator(event); - expect(GenerateReportValidator.errors).toBeNull(); - expect(isEventValid).toBe(true); + expect(() => validateGenerateReport(event, mockLogger)).not.toThrow(); }); it('should handle empty sender list', async () => { diff --git a/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts b/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts index 801fd41a3..6e85a0dc2 100644 --- a/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts +++ b/lambdas/report-scheduler/src/apis/scheduled-event-handler.ts @@ -1,7 +1,6 @@ import { EventPublisher } from 'utils'; import { ISenderManagement } from 'sender-management'; -import { GenerateReport } from 'digital-letters-events'; -import GenerateReportValidator from 'digital-letters-events/GenerateReport.js'; +import { GenerateReport, validateGenerateReport } from 'digital-letters-events'; import { randomUUID } from 'node:crypto'; export type CreateHandlerDependencies = { @@ -37,7 +36,7 @@ export const createHandler = ({ recordedtime: new Date().toISOString(), severitynumber: 2, })), - GenerateReportValidator, + validateGenerateReport, ); }; }; diff --git a/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts index 49f50f743..f31d41cc2 100644 --- a/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts +++ b/lambdas/ttl-create-lambda/src/__tests__/apis/sqs-trigger-lambda.test.ts @@ -1,8 +1,7 @@ import { messageDownloadedEvent } from '__tests__/data'; import { createHandler } from 'apis/sqs-trigger-lambda'; import type { SQSEvent } from 'aws-lambda'; -import { ItemEnqueued } from 'digital-letters-events'; -import itemEnqueuedValidator from 'digital-letters-events/ItemEnqueued.js'; +import { ItemEnqueued, validateItemEnqueued } from 'digital-letters-events'; import { randomUUID } from 'node:crypto'; jest.mock('node:crypto', () => ({ @@ -60,12 +59,14 @@ describe('createHandler', () => { expect(createTtl.send).toHaveBeenCalledWith(messageDownloadedEvent); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [itemEnqueuedEvent], - itemEnqueuedValidator, + validateItemEnqueued, ); const publishedEvent = eventPublisher.sendEvents.mock.lastCall?.[0]; expect(publishedEvent).toHaveLength(1); - expect(itemEnqueuedValidator(publishedEvent?.[0])).toBeTruthy(); + expect(() => + validateItemEnqueued(publishedEvent?.[0], logger), + ).not.toThrow(); expect(logger.info).toHaveBeenCalledWith({ description: 'Processed SQS Event.', @@ -180,7 +181,7 @@ describe('createHandler', () => { expect(createTtl.send).toHaveBeenCalledTimes(3); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [itemEnqueuedEvent, itemEnqueuedEvent, itemEnqueuedEvent], - itemEnqueuedValidator, + validateItemEnqueued, ); expect(logger.info).toHaveBeenCalledWith({ description: 'Processed SQS Event.', @@ -207,7 +208,7 @@ describe('createHandler', () => { expect(res.batchItemFailures).toEqual([]); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [itemEnqueuedEvent, itemEnqueuedEvent], - itemEnqueuedValidator, + validateItemEnqueued, ); expect(logger.warn).toHaveBeenCalledWith({ description: 'Some events failed to publish', @@ -230,7 +231,7 @@ describe('createHandler', () => { expect(res.batchItemFailures).toEqual([]); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [itemEnqueuedEvent], - itemEnqueuedValidator, + validateItemEnqueued, ); expect(logger.warn).toHaveBeenCalledWith({ err: publishError, @@ -279,7 +280,7 @@ describe('createHandler', () => { ]); expect(eventPublisher.sendEvents).toHaveBeenCalledWith( [itemEnqueuedEvent], - itemEnqueuedValidator, + validateItemEnqueued, ); expect(logger.info).toHaveBeenCalledWith({ description: 'Processed SQS Event.', diff --git a/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts b/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts index bb68c78e2..1ed224098 100644 --- a/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts +++ b/lambdas/ttl-create-lambda/src/apis/sqs-trigger-lambda.ts @@ -6,10 +6,10 @@ import type { import { randomUUID } from 'node:crypto'; import type { CreateTtl, CreateTtlOutcome } from 'app/create-ttl'; import { EventPublisher, Logger } from 'utils'; -import itemEnqueuedValidator from 'digital-letters-events/ItemEnqueued.js'; import { ItemEnqueued, MESHInboxMessageDownloaded, + validateItemEnqueued, validateMESHInboxMessageDownloaded, } from 'digital-letters-events'; @@ -102,7 +102,7 @@ export const createHandler = ({ messageUri: event.data.messageUri, }, })), - itemEnqueuedValidator, + validateItemEnqueued, ); if (failedEvents.length > 0) { logger.warn({ diff --git a/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts b/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts index 208498b97..ef3bee90b 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/__tests__/apis/dynamodb-stream-handler.test.ts @@ -3,7 +3,7 @@ import { EventPublisher, Logger } from 'utils'; import { mock } from 'jest-mock-extended'; import { createHandler } from 'apis/dynamodb-stream-handler'; import { Dlq } from 'app/dlq'; -import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; +import { validateItemDequeued } from 'digital-letters-events'; const logger = mock(); const eventPublisher = mock(); @@ -123,12 +123,14 @@ describe('createHandler', () => { }), }), ], - itemDequeuedValidator, + validateItemDequeued, ); const publishedEvent = eventPublisher.sendEvents.mock.lastCall?.[0]; expect(publishedEvent).toHaveLength(1); - expect(itemDequeuedValidator(publishedEvent?.[0])).toBeTruthy(); + expect(() => + validateItemDequeued(publishedEvent?.[0], logger), + ).not.toThrow(); expect(result).toEqual({}); }); @@ -396,7 +398,7 @@ describe('createHandler', () => { }), }), ], - itemDequeuedValidator, + validateItemDequeued, ); expect(result).toEqual({}); }); diff --git a/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts b/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts index b5c204bc8..ead29c79e 100644 --- a/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts +++ b/lambdas/ttl-handle-expiry-lambda/src/apis/dynamodb-stream-handler.ts @@ -7,9 +7,9 @@ import type { } from 'aws-lambda'; import { ItemDequeued, + validateItemDequeued, validateMESHInboxMessageDownloaded, } from 'digital-letters-events'; -import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; import { randomUUID } from 'node:crypto'; import { $TtlDynamodbRecord, EventPublisher, Logger } from 'utils'; @@ -92,7 +92,7 @@ export const createHandler = ({ }, }, ], - itemDequeuedValidator, + validateItemDequeued, ); } } catch (error) { diff --git a/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts b/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts index cb6f44e6b..34b081e82 100644 --- a/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts +++ b/utils/utils/src/__tests__/event-publisher/event-publisher.test.ts @@ -101,7 +101,9 @@ describe('Event Publishing', () => { }); const publisher = new EventPublisher(testConfig); - const result = await publisher.sendEvents(events, () => false); + const result = await publisher.sendEvents(events, () => { + throw new Error('Some validation error'); + }); expect(result).toEqual([]); expect(eventBridgeMock.calls()).toHaveLength(0); @@ -186,7 +188,9 @@ describe('Event Publishing', () => { }); const publisher = new EventPublisher(testConfig); - const result = await publisher.sendEvents(events, () => false); + const result = await publisher.sendEvents(events, () => { + throw new Error('Some validation error'); + }); expect(result).toEqual([event]); expect(eventBridgeMock.calls()).toHaveLength(0); @@ -197,7 +201,9 @@ describe('Event Publishing', () => { sqsMock.on(SendMessageBatchCommand).rejects(new Error('DLQ error')); const publisher = new EventPublisher(testConfig); - const result = await publisher.sendEvents(events, () => false); + const result = await publisher.sendEvents(events, () => { + throw new Error('Some validation error'); + }); expect(result).toEqual(events); expect(eventBridgeMock.calls()).toHaveLength(0); @@ -359,14 +365,14 @@ describe('Event Publishing', () => { }); const publisher = new EventPublisher(testConfig); - const result = await publisher.sendEvents( - allEvents, - (e) => - !( - e.id.includes('22222222-2222-2222-2222') || - e.id.includes('33333333-3333-3333-3333') - ), - ); + const result = await publisher.sendEvents(allEvents, (e) => { + if ( + e.id.includes('22222222-2222-2222-2222') || + e.id.includes('33333333-3333-3333-3333') + ) { + throw new Error('Some validation error'); + } + }); expect(result).toHaveLength( invalidAndDlqError.length + eventBridgeAndDlqError.length, diff --git a/utils/utils/src/event-publisher/event-publisher.ts b/utils/utils/src/event-publisher/event-publisher.ts index e7b6b45f6..e4fdc20c9 100644 --- a/utils/utils/src/event-publisher/event-publisher.ts +++ b/utils/utils/src/event-publisher/event-publisher.ts @@ -20,7 +20,7 @@ export interface EventPublisherDependencies { type PublishableEvent = { id: string; source: string; type: string }; -type EventValidationFunction = { (event: T): boolean; errors?: any[] }; +type EventValidationFunction = (event: T, logger: Logger) => void; export class EventPublisher { private readonly eventBridge: EventBridgeClient; @@ -201,16 +201,11 @@ export class EventPublisher { const invalidEvents: T[] = []; for (const event of events) { - const isEventValid = eventValidator(event); - if (isEventValid) { + try { + eventValidator(event, this.logger); validEvents.push(event); - } else { + } catch { invalidEvents.push(event); - - this.logger.info({ - description: 'Error parsing event', - error: eventValidator.errors, - }); } } From 9991a570d691f2ed8b27c14d7807dc47c516d2f6 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Wed, 25 Mar 2026 16:03:42 +0000 Subject: [PATCH 08/17] CCM-13675: Get rid of js validators in lamdbas --- .../src/__tests__/app/print-sender.test.ts | 8 ++------ lambdas/print-sender-lambda/src/app/print-sender.ts | 13 +++++++++++-- .../print-status-handler/src/apis/sqs-handler.ts | 5 ++++- .../src/generate-guard-functions.ts | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lambdas/print-sender-lambda/src/__tests__/app/print-sender.test.ts b/lambdas/print-sender-lambda/src/__tests__/app/print-sender.test.ts index 529e571e2..639298964 100644 --- a/lambdas/print-sender-lambda/src/__tests__/app/print-sender.test.ts +++ b/lambdas/print-sender-lambda/src/__tests__/app/print-sender.test.ts @@ -119,10 +119,7 @@ describe('PrintSender', () => { mockEventPublisher.sendEvents.mockImplementation( async (events, validator) => { - const isValid = validator(events[0]); - if (!isValid) { - throw new Error('Event validation failed'); - } + validator(events[0], mockLogger); return []; }, ); @@ -160,12 +157,11 @@ describe('PrintSender', () => { const [[events, eventValidator]] = mockEventPublisher.sendEvents.mock.calls; const event = events[0] as LetterRequestPreparedEvent; - const validationResult = eventValidator(event); + expect(() => eventValidator(event, mockLogger)).not.toThrow(); expect(event.source).toBe( '/data-plane/digital-letters/staging-account/staging', ); - expect(validationResult).toBe(true); }); }); }); diff --git a/lambdas/print-sender-lambda/src/app/print-sender.ts b/lambdas/print-sender-lambda/src/app/print-sender.ts index 2de2c563e..fe83e8a71 100644 --- a/lambdas/print-sender-lambda/src/app/print-sender.ts +++ b/lambdas/print-sender-lambda/src/app/print-sender.ts @@ -1,4 +1,4 @@ -import { PDFAnalysed } from 'digital-letters-events'; +import { InvalidEvent, PDFAnalysed } from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; import { randomUUID } from 'node:crypto'; import { @@ -53,7 +53,16 @@ export class PrintSender { await this.eventPublisher.sendEvents( [letterPreparedEvent], - (event) => $LetterRequestPreparedEvent.safeParse(event).success, + (event, logger) => { + const parseResult = $LetterRequestPreparedEvent.safeParse(event); + if (!parseResult.success) { + logger.error({ + err: parseResult.error, + description: 'Error parsing LetterRequestPreparedEvent event', + }); + throw new InvalidEvent('Invalid LetterRequestPreparedEvent'); + } + }, ); } catch (error) { this.logger.error({ diff --git a/lambdas/print-status-handler/src/apis/sqs-handler.ts b/lambdas/print-status-handler/src/apis/sqs-handler.ts index dee78a0cb..d75d7a678 100644 --- a/lambdas/print-status-handler/src/apis/sqs-handler.ts +++ b/lambdas/print-status-handler/src/apis/sqs-handler.ts @@ -9,7 +9,10 @@ import { $LetterEvent, LetterEvent, } from '@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events'; -import { PrintLetterTransitioned, validatePrintLetterTransitioned } from 'digital-letters-events'; +import { + PrintLetterTransitioned, + validatePrintLetterTransitioned, +} from 'digital-letters-events'; import { EventPublisher, Logger } from 'utils'; export interface HandlerDependencies { diff --git a/src/typescript-schema-generator/src/generate-guard-functions.ts b/src/typescript-schema-generator/src/generate-guard-functions.ts index 6f4e57502..f8dea3180 100644 --- a/src/typescript-schema-generator/src/generate-guard-functions.ts +++ b/src/typescript-schema-generator/src/generate-guard-functions.ts @@ -19,7 +19,7 @@ export async function generateGuardFunctions() { const validatorVariableName = `event${typeName}Validator`; let guardFunction = `import ${validatorVariableName} from 'digital-letters-events/${typeName}.js'\n`; - guardFunction += `import { InvalidEvent, type ${typeName} } from 'digital-letters-events';`; + guardFunction += `import { InvalidEvent, type ${typeName} } from 'digital-letters-events';\n`; guardFunction += `import { Logger } from 'utils';\n\n`; guardFunction += `export function validate${typeName}(\n`; From 9a29a8f329604839211a512063c9ea80c671f867 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Thu, 26 Mar 2026 11:58:24 +0000 Subject: [PATCH 09/17] CCM-13675: Address review comments --- lambdas/print-sender-lambda/src/app/print-sender.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambdas/print-sender-lambda/src/app/print-sender.ts b/lambdas/print-sender-lambda/src/app/print-sender.ts index fe83e8a71..5772d5cdf 100644 --- a/lambdas/print-sender-lambda/src/app/print-sender.ts +++ b/lambdas/print-sender-lambda/src/app/print-sender.ts @@ -56,7 +56,7 @@ export class PrintSender { (event, logger) => { const parseResult = $LetterRequestPreparedEvent.safeParse(event); if (!parseResult.success) { - logger.error({ + logger.warn({ err: parseResult.error, description: 'Error parsing LetterRequestPreparedEvent event', }); From de8d03d30808d275fc84ad69c91478f5f4358c3c Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Thu, 26 Mar 2026 13:33:19 +0000 Subject: [PATCH 10/17] CCM-13675: Fix component tests --- .../core-notify.component.spec.ts | 14 +++++++------ .../file-scanner.component.spec.ts | 6 +++--- .../mesh-acknowledge.component.spec.ts | 10 ++++++---- .../mesh-poll-download.component.spec.ts | 4 ++-- .../pdm-poll.component.spec.ts | 16 ++++++++------- .../pdm-uploader.component.spec.ts | 6 +++--- .../print-analyser.component.spec.ts | 5 ++--- .../print-sender.component.spec.ts | 5 ++--- .../send-reports-trust.component.spec.ts | 4 ++-- .../ttl-create.component.spec.ts | 12 ++++++----- tests/playwright/helpers/report-helpers.ts | 20 +++++++++---------- 11 files changed, 54 insertions(+), 48 deletions(-) diff --git a/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts b/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts index fb26d8032..de9e137fd 100644 --- a/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/core-notify.component.spec.ts @@ -9,8 +9,10 @@ import { SENDER_ID_THAT_TRIGGERS_ERROR_IN_NOTIFY_SANDBOX, SENDER_ID_VALID_FOR_NOTIFY_SANDBOX, } from 'constants/tests-constants'; -import { PDMResourceAvailable } from 'digital-letters-events'; -import messagePDMResourceAvailableValidator from 'digital-letters-events/PDMResourceAvailable.js'; +import { + PDMResourceAvailable, + validatePDMResourceAvailable, +} from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; @@ -63,7 +65,7 @@ test.describe('Digital Letters - Core Notify', () => { }, }, ], - messagePDMResourceAvailableValidator, + validatePDMResourceAvailable, ); // Verify the event is processed and a message appears in the Lambda logs @@ -116,7 +118,7 @@ test.describe('Digital Letters - Core Notify', () => { }, }, ], - messagePDMResourceAvailableValidator, + validatePDMResourceAvailable, ); // Verify the event is processed and a message appears in the Lambda logs @@ -168,7 +170,7 @@ test.describe('Digital Letters - Core Notify', () => { }, }, ], - messagePDMResourceAvailableValidator, + validatePDMResourceAvailable, ); // Verify the event is published in the event bus @@ -206,7 +208,7 @@ test.describe('Digital Letters - Core Notify', () => { }, }, ], - messagePDMResourceAvailableValidator, + validatePDMResourceAvailable, ); await Promise.all([ diff --git a/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts b/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts index 48b277fb7..a1c7e5836 100644 --- a/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/file-scanner.component.spec.ts @@ -5,7 +5,6 @@ import { PREFIX_DL_FILES, REGION, } from 'constants/backend-constants'; -import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; @@ -15,6 +14,7 @@ import { getS3ObjectMetadata, putDataS3, } from 'utils'; +import { validateItemDequeued } from 'digital-letters-events'; const DOCUMENT_REFERENCE_BUCKET = `nhs-${process.env.AWS_ACCOUNT_ID}-${REGION}-${ENV}-dl-pii-data`; const UNSCANNED_FILES_BUCKET = `nhs-${process.env.AWS_ACCOUNT_ID}-${REGION}-main-acct-digi-unscanned-files`; @@ -77,7 +77,7 @@ test('should extract PDF from DocumentReference and store in unscanned bucket wi }, }, ], - itemDequeuedValidator, + validateItemDequeued, ); await expectToPassEventually(async () => { @@ -143,7 +143,7 @@ test('should handle validation errors by sending messages to DLQ', async () => { }, }, ], - itemDequeuedValidator, + validateItemDequeued, ); // Verify the file was NOT processed successfully diff --git a/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts b/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts index 8fe63d32b..a55e148bb 100644 --- a/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/mesh-acknowledge.component.spec.ts @@ -5,8 +5,10 @@ import { NON_PII_S3_BUCKET_NAME, } from 'constants/backend-constants'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; -import { MESHInboxMessageDownloaded } from 'digital-letters-events'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; +import { + MESHInboxMessageDownloaded, + validateMESHInboxMessageDownloaded, +} from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; @@ -68,7 +70,7 @@ test.describe('Digital Letters - Mesh Acknowledger', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); // The mailbox ID matches the Mock MESH config in SSM. @@ -126,7 +128,7 @@ test.describe('Digital Letters - Mesh Acknowledger', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); await expectMessageContainingString( diff --git a/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts b/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts index 430b8512b..7fde42e51 100644 --- a/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/mesh-poll-download.component.spec.ts @@ -13,8 +13,8 @@ import { invokeLambda } from 'helpers/lambda-helpers'; import { downloadFromS3, uploadToS3 } from 'helpers/s3-helpers'; import { expectMessageContainingString } from 'helpers/sqs-helpers'; import { v4 as uuidv4 } from 'uuid'; -import messageMessageReceived from 'digital-letters-events/MESHInboxMessageReceived.js'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; +import { validateMESHInboxMessageReceived } from 'digital-letters-events'; test.describe('Digital Letters - MESH Poll and Download', () => { const senderId = SENDER_ID_SKIPS_NOTIFY; @@ -142,7 +142,7 @@ test.describe('Digital Letters - MESH Poll and Download', () => { }, }, ], - messageMessageReceived, + validateMESHInboxMessageReceived, ); await expectMessageContainingString( diff --git a/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts b/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts index 0d0d04548..8d31cfb8d 100644 --- a/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/pdm-poll.component.spec.ts @@ -4,8 +4,10 @@ import { PDM_POLL_DLQ_NAME, PDM_POLL_LAMBDA_LOG_GROUP_NAME, } from 'constants/backend-constants'; -import pdmResourceSubmittedValidator from 'digital-letters-events/PDMResourceSubmitted.js'; -import pdmResourceUnavailableValidator from 'digital-letters-events/PDMResourceUnavailable.js'; +import { + validatePDMResourceSubmitted, + validatePDMResourceUnavailable, +} from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; @@ -65,7 +67,7 @@ test.describe('PDM Poll', () => { }, }, ], - pdmResourceSubmittedValidator, + validatePDMResourceSubmitted, ); await expectToPassEventually(async () => { @@ -102,7 +104,7 @@ test.describe('PDM Poll', () => { }, }, ], - pdmResourceSubmittedValidator, + validatePDMResourceSubmitted, ); await expectToPassEventually(async () => { @@ -141,7 +143,7 @@ test.describe('PDM Poll', () => { }, }, ], - pdmResourceUnavailableValidator, + validatePDMResourceUnavailable, ); await expectToPassEventually(async () => { @@ -179,7 +181,7 @@ test.describe('PDM Poll', () => { }, }, ], - pdmResourceUnavailableValidator, + validatePDMResourceUnavailable, ); await expectToPassEventually(async () => { @@ -216,7 +218,7 @@ test.describe('PDM Poll', () => { }, }, ], - pdmResourceUnavailableValidator, + validatePDMResourceUnavailable, ); await expectToPassEventually(async () => { diff --git a/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts b/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts index e5ba36dcd..5b2993eb2 100644 --- a/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/pdm-uploader.component.spec.ts @@ -5,7 +5,6 @@ import { PDM_UPLOADER_DLQ_NAME, PDM_UPLOADER_LAMBDA_LOG_GROUP_NAME, } from 'constants/backend-constants'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; @@ -13,6 +12,7 @@ import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; import { v4 as uuidv4 } from 'uuid'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; import { putDataS3 } from 'utils'; +import { validateMESHInboxMessageDownloaded } from 'digital-letters-events'; const pdmRequest = { resourceType: 'DocumentReference', @@ -84,7 +84,7 @@ test.describe('Digital Letters - Upload to PDM', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); await expectToPassEventually(async () => { @@ -150,7 +150,7 @@ test.describe('Digital Letters - Upload to PDM', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); await expectToPassEventually(async () => { diff --git a/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts index b71ad2101..46720477c 100644 --- a/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-analyser.component.spec.ts @@ -10,8 +10,7 @@ import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; import { fivePagePdf } from 'helpers/pdf-helpers'; import { v4 as uuidv4 } from 'uuid'; -import fileSafeValidator from 'digital-letters-events/FileSafe.js'; -import { FileSafe } from 'digital-letters-events'; +import { FileSafe, validateFileSafe } from 'digital-letters-events'; import { expectMessageContainingString, purgeQueue } from 'helpers/sqs-helpers'; import { putFileS3 } from 'utils'; @@ -64,7 +63,7 @@ test.describe('Print analyser', () => { }, }; - await eventPublisher.sendEvents([event], fileSafeValidator); + await eventPublisher.sendEvents([event], validateFileSafe); await expectToPassEventually(async () => { const eventLogEntry = await getLogsFromCloudwatch( diff --git a/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts b/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts index 0988c5a63..b761c54ab 100644 --- a/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/print-sender.component.spec.ts @@ -4,8 +4,7 @@ import { PRINT_SENDER_DLQ_NAME, PRINT_SENDER_LAMBDA_LOG_GROUP_NAME, } from 'constants/backend-constants'; -import { PDFAnalysed } from 'digital-letters-events'; -import pdfAnalysedValidator from 'digital-letters-events/PDFAnalysed.js'; +import { PDFAnalysed, validatePDFAnalysed } from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; import expectToPassEventually from 'helpers/expectations'; @@ -52,7 +51,7 @@ test.describe('Digital Letters - Print Sender', () => { }, }, ], - pdfAnalysedValidator, + validatePDFAnalysed, ); // Verify letter prepared event published diff --git a/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts b/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts index 5c5bd417b..2a8311680 100644 --- a/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/send-reports-trust.component.spec.ts @@ -11,8 +11,8 @@ import expectToPassEventually from 'helpers/expectations'; import { downloadFromS3, uploadToS3 } from 'helpers/s3-helpers'; import { expectMessageContainingString } from 'helpers/sqs-helpers'; import { v4 as uuidv4 } from 'uuid'; -import reportGenerated from 'digital-letters-events/ReportGenerated.js'; import { SENDER_ID_SKIPS_NOTIFY } from 'constants/tests-constants'; +import { validateReportGenerated } from 'digital-letters-events'; test.describe('Digital Letters - Send reports to Trust', () => { const senderId = SENDER_ID_SKIPS_NOTIFY; @@ -44,7 +44,7 @@ test.describe('Digital Letters - Send reports to Trust', () => { }, }, ], - reportGenerated, + validateReportGenerated, ); } diff --git a/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts b/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts index 5afee577d..6a5d35fa2 100644 --- a/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts +++ b/tests/playwright/digital-letters-component-tests/ttl-create.component.spec.ts @@ -8,8 +8,10 @@ import { SENDER_ID_SKIPS_NOTIFY, SENDER_ID_VALID_FOR_NOTIFY_SANDBOX, } from 'constants/tests-constants'; -import { MESHInboxMessageDownloaded } from 'digital-letters-events'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; +import { + MESHInboxMessageDownloaded, + validateMESHInboxMessageDownloaded, +} from 'digital-letters-events'; import { getLogsFromCloudwatch } from 'helpers/cloudwatch-helpers'; import { getTtl } from 'helpers/dynamodb-helpers'; import eventPublisher from 'helpers/event-bus-helpers'; @@ -61,7 +63,7 @@ test.describe('Digital Letters - Create TTL', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); // Verify TTL created @@ -102,7 +104,7 @@ test.describe('Digital Letters - Create TTL', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); // Verify TTL created @@ -185,7 +187,7 @@ test.describe('Digital Letters - Create TTL', () => { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); await Promise.all([ diff --git a/tests/playwright/helpers/report-helpers.ts b/tests/playwright/helpers/report-helpers.ts index f1a9f17c8..f48428da1 100644 --- a/tests/playwright/helpers/report-helpers.ts +++ b/tests/playwright/helpers/report-helpers.ts @@ -15,12 +15,12 @@ import { ItemDequeued, MESHInboxMessageDownloaded, PrintLetterTransitioned, + validateDigitalLetterRead, + validateGenerateReport, + validateItemDequeued, + validateMESHInboxMessageDownloaded, + validatePrintLetterTransitioned, } from 'digital-letters-events'; -import generateReportValidator from 'digital-letters-events/GenerateReport.js'; -import digitalLetterReadValidator from 'digital-letters-events/DigitalLetterRead.js'; -import messageDownloadedValidator from 'digital-letters-events/MESHInboxMessageDownloaded.js'; -import itemDequeuedValidator from 'digital-letters-events/ItemDequeued.js'; -import printLetterTransitionedValidator from 'digital-letters-events/PrintLetterTransitioned.js'; import { QueryExecutionState, getQueryState, @@ -115,7 +115,7 @@ export function publishEventForScenario(scenario: ReportScenario) { scenario.senderId, ), ], - digitalLetterReadValidator, + validateDigitalLetterRead, ); } else if (EventStatus.Unread === status) { eventPublisher.sendEvents( @@ -127,7 +127,7 @@ export function publishEventForScenario(scenario: ReportScenario) { scenario.senderId, ), ], - itemDequeuedValidator, + validateItemDequeued, ); } break; @@ -143,7 +143,7 @@ export function publishEventForScenario(scenario: ReportScenario) { scenario.senderId, ), ], - printLetterTransitionedValidator, + validatePrintLetterTransitioned, ); break; } @@ -186,7 +186,7 @@ export async function publishGenerateReport( }, }, ], - generateReportValidator, + validateGenerateReport, ); } @@ -222,7 +222,7 @@ export async function publishEventNotInReports(senderId: string) { }, }, ], - messageDownloadedValidator, + validateMESHInboxMessageDownloaded, ); } From ca67c3ba37bdbb2fbbfd49ace3feb7cf9ceae0ba Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Fri, 27 Mar 2026 10:31:16 +0000 Subject: [PATCH 11/17] CCM-13675: Fix component tests --- lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts b/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts index 44c8e9fe0..9a58a4c20 100644 --- a/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts +++ b/lambdas/pdm-poll-lambda/src/apis/sqs-handler.ts @@ -9,6 +9,7 @@ import { PDMResourceRetriesExceeded, PDMResourceSubmitted, PDMResourceUnavailable, + validatePDMResourceAvailable, validatePDMResourceRetriesExceeded, validatePDMResourceSubmitted, validatePDMResourceUnavailable, @@ -193,7 +194,7 @@ export const createHandler = ({ availableEvents.length > 0 && eventPublisher.sendEvents( availableEvents, - validatePDMResourceUnavailable, + validatePDMResourceAvailable, ), unavailableEvents.length > 0 && eventPublisher.sendEvents( From fd641027b5d6c4ddf5d6b60830e55d8bf8215751 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Fri, 27 Mar 2026 16:44:17 +0000 Subject: [PATCH 12/17] CCM-13675: Address review comments --- .../src/__tests__/apis/sqs-handler.test.ts | 3 ++ src/digital-letters-events/tsconfig.json | 5 ++- .../src/generate-guard-functions.ts | 39 +++++++++---------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts b/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts index c01f79ce4..800803e40 100644 --- a/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/print-analyser/src/__tests__/apis/sqs-handler.test.ts @@ -114,6 +114,9 @@ describe('SQS Handler', () => { const result = await handler(event); + expect(logger.child).toHaveBeenCalledWith({ + messageReference: fileSafeEvent.data.messageReference, + }); expect(mockChildLogger.error).toHaveBeenCalledWith({ err: expect.arrayContaining([ expect.objectContaining({ diff --git a/src/digital-letters-events/tsconfig.json b/src/digital-letters-events/tsconfig.json index c2099752d..ba1a1fd13 100644 --- a/src/digital-letters-events/tsconfig.json +++ b/src/digital-letters-events/tsconfig.json @@ -10,6 +10,9 @@ "extends": "@tsconfig/node22/tsconfig.json", "include": [ "validators/**/*", - "types/**/*" + "types/**/*", + "guard-functions/**/*", + "index.ts", + "errors/**/*" ] } diff --git a/src/typescript-schema-generator/src/generate-guard-functions.ts b/src/typescript-schema-generator/src/generate-guard-functions.ts index f8dea3180..f09be210e 100644 --- a/src/typescript-schema-generator/src/generate-guard-functions.ts +++ b/src/typescript-schema-generator/src/generate-guard-functions.ts @@ -18,29 +18,28 @@ export async function generateGuardFunctions() { const validatorVariableName = `event${typeName}Validator`; - let guardFunction = `import ${validatorVariableName} from 'digital-letters-events/${typeName}.js'\n`; - guardFunction += `import { InvalidEvent, type ${typeName} } from 'digital-letters-events';\n`; - guardFunction += `import { Logger } from 'utils';\n\n`; - - guardFunction += `export function validate${typeName}(\n`; - guardFunction += ` event: unknown,\n`; - guardFunction += ` logger: Logger,\n`; - guardFunction += `): asserts event is ${typeName} {\n`; - guardFunction += ` if (!${validatorVariableName}(event)) {\n`; - guardFunction += ` logger.error({\n`; - guardFunction += ` err: ${validatorVariableName}.errors,\n`; - guardFunction += ` description: 'Error parsing ${typeName} event',\n`; - guardFunction += ` });\n`; - guardFunction += ` throw new InvalidEvent(${validatorVariableName}.errors);\n`; - guardFunction += ` }\n`; - guardFunction += `}\n`; - - const typeDeclarationName = `${typeName}`; - const typeDeclarationFilename = `${typeDeclarationName}.ts`; + const guardFunction = `import ${validatorVariableName} from 'digital-letters-events/${typeName}.js'; +import { InvalidEvent, type ${typeName} } from 'digital-letters-events'; +import { Logger } from 'utils'; + +export function validate${typeName}( + event: unknown, + logger: Logger, +): asserts event is ${typeName} { + if (!${validatorVariableName}(event)) { + logger.error({ + err: ${validatorVariableName}.errors, + description: 'Error parsing ${typeName} event', + }); + throw new InvalidEvent(${validatorVariableName}.errors); + } +}`; + + const typeDeclarationFilename = `${typeName}.ts`; writeFile(outputDir, typeDeclarationFilename, guardFunction); console.log(typeDeclarationFilename); - indexLines.push(`export * from './${typeDeclarationName}';`); + indexLines.push(`export * from './${typeName}';`); } console.groupEnd(); From 548b292cacca0c9a789d953e4622267c23812cb5 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Fri, 27 Mar 2026 17:11:15 +0000 Subject: [PATCH 13/17] CCM-13675: Address review comments --- src/digital-letters-events/package.json | 4 ---- .../src/generate-guard-functions.ts | 17 +++++++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/digital-letters-events/package.json b/src/digital-letters-events/package.json index c482fa68e..08e01ecdc 100644 --- a/src/digital-letters-events/package.json +++ b/src/digital-letters-events/package.json @@ -11,10 +11,6 @@ "exports": { ".": { "default": "./index.ts" - }, - "./*.js": { - "default": "./validators/*.js", - "types": "./validators/index.d.ts" } }, "name": "digital-letters-events", diff --git a/src/typescript-schema-generator/src/generate-guard-functions.ts b/src/typescript-schema-generator/src/generate-guard-functions.ts index f09be210e..d0c0a0452 100644 --- a/src/typescript-schema-generator/src/generate-guard-functions.ts +++ b/src/typescript-schema-generator/src/generate-guard-functions.ts @@ -18,22 +18,27 @@ export async function generateGuardFunctions() { const validatorVariableName = `event${typeName}Validator`; - const guardFunction = `import ${validatorVariableName} from 'digital-letters-events/${typeName}.js'; -import { InvalidEvent, type ${typeName} } from 'digital-letters-events'; + const guardFunction = `import ${validatorVariableName} from '../validators/${typeName}.js'; +import { type ${typeName} } from '../types'; +import { InvalidEvent } from '../errors'; import { Logger } from 'utils'; +import { ValidateFunction } from 'ajv'; + +const validator = ${validatorVariableName} as unknown as ValidateFunction; export function validate${typeName}( event: unknown, logger: Logger, ): asserts event is ${typeName} { - if (!${validatorVariableName}(event)) { + if (!validator(event)) { logger.error({ - err: ${validatorVariableName}.errors, + err: validator.errors, description: 'Error parsing ${typeName} event', }); - throw new InvalidEvent(${validatorVariableName}.errors); + throw new InvalidEvent(validator.errors); } -}`; +} +`; const typeDeclarationFilename = `${typeName}.ts`; writeFile(outputDir, typeDeclarationFilename, guardFunction); From ee7a2b723b635643254831ed8a0fd71a8452f5d5 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Mon, 30 Mar 2026 13:52:52 +0100 Subject: [PATCH 14/17] CCM-13675: Fix build --- src/digital-letters-events/package.json | 4 ++++ .../src/generate-guard-functions.ts | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/digital-letters-events/package.json b/src/digital-letters-events/package.json index 08e01ecdc..c482fa68e 100644 --- a/src/digital-letters-events/package.json +++ b/src/digital-letters-events/package.json @@ -11,6 +11,10 @@ "exports": { ".": { "default": "./index.ts" + }, + "./*.js": { + "default": "./validators/*.js", + "types": "./validators/index.d.ts" } }, "name": "digital-letters-events", diff --git a/src/typescript-schema-generator/src/generate-guard-functions.ts b/src/typescript-schema-generator/src/generate-guard-functions.ts index d0c0a0452..6cff242a6 100644 --- a/src/typescript-schema-generator/src/generate-guard-functions.ts +++ b/src/typescript-schema-generator/src/generate-guard-functions.ts @@ -18,7 +18,7 @@ export async function generateGuardFunctions() { const validatorVariableName = `event${typeName}Validator`; - const guardFunction = `import ${validatorVariableName} from '../validators/${typeName}.js'; + const guardFunction = `import ${validatorVariableName} from 'digital-letters-events/${typeName}.js'; import { type ${typeName} } from '../types'; import { InvalidEvent } from '../errors'; import { Logger } from 'utils'; @@ -40,11 +40,11 @@ export function validate${typeName}( } `; - const typeDeclarationFilename = `${typeName}.ts`; + const typeDeclarationFilename = `${typeName}Guard.ts`; writeFile(outputDir, typeDeclarationFilename, guardFunction); console.log(typeDeclarationFilename); - indexLines.push(`export * from './${typeName}';`); + indexLines.push(`export * from './${typeName}Guard';`); } console.groupEnd(); From a43fd07c382a6ce8a01f3cbaf2e983597118c30c Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Mon, 30 Mar 2026 14:38:31 +0100 Subject: [PATCH 15/17] CCM-13675: Fix build --- src/digital-letters-events/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/digital-letters-events/tsconfig.json b/src/digital-letters-events/tsconfig.json index ba1a1fd13..0a4341bab 100644 --- a/src/digital-letters-events/tsconfig.json +++ b/src/digital-letters-events/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "allowJs": true, "isolatedModules": true, - "outDir": "dist" + "outDir": "dist", + "rootDir": "." }, "exclude": [ "node_modules" From d149c9495acecea31faf5519f8f99f1e19077eb6 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Mon, 30 Mar 2026 14:46:23 +0100 Subject: [PATCH 16/17] CCM-13675: Fix compile issues --- .../src/__tests__/generate-guard-functions.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts b/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts index fb9da4b7a..399f2c9b9 100644 --- a/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts +++ b/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts @@ -43,7 +43,7 @@ describe('generate-guard-functions', () => { expect(typeDeclarationFiles.length).toBe(4); expect(typeDeclarationFiles).toEqual( - expect.arrayContaining(['index.ts', 'One.ts', 'Two.ts', 'Three.ts']), + expect.arrayContaining(['index.ts', 'OneGuard.ts', 'TwoGuard.ts', 'ThreeGuard.ts']), ); }); @@ -54,8 +54,8 @@ describe('generate-guard-functions', () => { path.join(outputDir, 'index.ts'), 'utf8', ); - expect(indexFileContents).toContain("export * from './One';"); - expect(indexFileContents).toContain("export * from './Two';"); - expect(indexFileContents).toContain("export * from './Three';"); + expect(indexFileContents).toContain("export * from './OneGuard';"); + expect(indexFileContents).toContain("export * from './TwoGuard';"); + expect(indexFileContents).toContain("export * from './ThreeGuard';"); }); }); From 65f2bb8380bcdaed5254332217f09c5e9b49488f Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Mon, 30 Mar 2026 14:53:25 +0100 Subject: [PATCH 17/17] CCM-13675: Fix compile issues --- .../src/__tests__/generate-guard-functions.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts b/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts index 399f2c9b9..ab8418fca 100644 --- a/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts +++ b/src/typescript-schema-generator/src/__tests__/generate-guard-functions.test.ts @@ -43,7 +43,12 @@ describe('generate-guard-functions', () => { expect(typeDeclarationFiles.length).toBe(4); expect(typeDeclarationFiles).toEqual( - expect.arrayContaining(['index.ts', 'OneGuard.ts', 'TwoGuard.ts', 'ThreeGuard.ts']), + expect.arrayContaining([ + 'index.ts', + 'OneGuard.ts', + 'TwoGuard.ts', + 'ThreeGuard.ts', + ]), ); });