Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions infrastructure/terraform/components/api/ddb_table_letters.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
resource "aws_dynamodb_table" "letters" {
name = "${local.csi}-letters"
billing_mode = "PAY_PER_REQUEST"
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"

hash_key = "supplierId"
range_key = "id"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
resource "aws_lambda_event_source_mapping" "letter_updates_transformer_kinesis" {
event_source_arn = aws_kinesis_stream.letter_change_stream.arn
function_name = module.letter_updates_transformer.function_arn
starting_position = "LATEST"
batch_size = 10
maximum_batching_window_in_seconds = 1

depends_on = [
module.letter_updates_transformer # ensures updates transformer exists
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
resource "aws_kinesis_stream" "letter_change_stream" {
name = "${local.csi}-letter-change-stream"
shard_count = 1
retention_period = 24
}

resource "aws_dynamodb_kinesis_streaming_destination" "letter_streaming_destination" {
stream_arn = aws_kinesis_stream.letter_change_stream.arn
table_name = aws_dynamodb_table.letters.name
approximate_creation_date_time_precision = "MILLISECOND"
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,35 +36,39 @@ module "letter_updates_transformer" {
log_subscription_role_arn = local.acct.log_subscription_role_arn

lambda_env_vars = merge(local.common_lambda_env_vars, {
EVENTPUB_SNS_TOPIC_ARN = module.eventpub.sns_topic.arn
EVENTPUB_SNS_TOPIC_ARN = "${module.eventpub.sns_topic.arn}"
})
}

data "aws_iam_policy_document" "letter_updates_transformer_lambda" {
statement {
sid = "KMSPermissions"
sid = "AllowSNSPublish"
effect = "Allow"

actions = [
"kms:Decrypt",
"kms:GenerateDataKey",
"sns:Publish"
]

resources = [
module.kms.key_arn,
module.eventpub.sns_topic.arn
]
}

statement {
sid = "AllowSNSPublish"
sid = "AllowKinesisGet"
effect = "Allow"

actions = [
"sns:Publish"
"kinesis:GetRecords",
"kinesis:GetShardIterator",
"kinesis:DescribeStream",
"kinesis:DescribeStreamSummary",
"kinesis:ListShards",
"kinesis:ListStreams",
]

resources = [
module.eventpub.sns_topic.arn
aws_kinesis_stream.letter_change_stream.arn
]
}
}
2 changes: 1 addition & 1 deletion internal/events/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@
"typecheck": "tsc --noEmit"
},
"types": "dist/index.d.ts",
"version": "1.0.3"
"version": "1.0.4"
}
10 changes: 10 additions & 0 deletions internal/events/src/events/__tests__/event-envelope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ describe("EventEnvelope schema validation", () => {
const baseValidEnvelope: Envelope = {
dataschema:
"https://notify.nhs.uk/cloudevents/schemas/supplier-api/order.READ.1.0.0.schema.json",
dataschemaversion: "1.0.0",
specversion: "1.0",
id: "6f1c2a53-3d54-4a0a-9a0b-0e9ae2d4c111",
source: "/data-plane/supplier-api/ordering",
subject: "order/769acdd4",
type: "uk.nhs.notify.supplier-api.order.READ.v1",
plane: "data",
time: "2025-10-01T10:15:30.000Z",
datacontenttype: "application/json",
data: {
"notify-payload": {
"notify-data": { nhsNumber: "9434765919" },
Expand Down Expand Up @@ -241,10 +244,13 @@ describe("EventEnvelope schema validation", () => {
specversion: "1.0" as const,
id: "6f1c2a53-3d54-4a0a-9a0b-0e02b2c3d479",
type: "uk.nhs.notify.supplier-api.letter.CREATED.v1" as const,
plane: "data",
dataschema:
"https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.CREATED.1.0.0.schema.json",
dataschemaversion: "1.0.0",
source: "/data-plane/supplier-api/letters",
time: "2025-10-01T10:15:30.000Z",
datacontenttype: "application/json",
data: { status: "CREATED" },
traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
recordedtime: "2025-10-01T10:15:30.250Z",
Expand Down Expand Up @@ -294,11 +300,14 @@ describe("EventEnvelope schema validation", () => {
specversion: "1.0" as const,
id: "6f1c2a53-3d54-4a0a-9a0b-0e9ae2d4c111",
type: "uk.nhs.notify.supplier-api.order.READ.v1" as const,
plane: "data",
dataschema:
"https://notify.nhs.uk/cloudevents/schemas/supplier-api/order.READ.1.0.0.schema.json",
dataschemaversion: "1.0.0",
source: "/data-plane/supplier-api/ordering",
subject: "order/769acdd4",
time: "2025-10-01T10:15:30.000Z",
datacontenttype: "application/json",
data: { status: "READ" },
traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
recordedtime: "2025-10-01T10:15:30.250Z",
Expand Down Expand Up @@ -341,6 +350,7 @@ describe("EventEnvelope schema validation", () => {
type: "uk.nhs.notify.supplier-api.order.read.v1" as const,
dataschema:
"https://notify.nhs.uk/cloudevents/schemas/supplier-api/order.read.1.0.0.schema.json",
dataschemaversion: "1.0.0",
source: "/data-plane/supplier-api/ordering",
subject: "prefix/letter-rendering/order/769acdd4",
time: "2025-10-01T10:15:30.000Z",
Expand Down
1 change: 1 addition & 0 deletions internal/events/src/events/__tests__/mi-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe("MI event validations", () => {
expect(event).toEqual(
expect.objectContaining({
type: "uk.nhs.notify.supplier-api.mi.SUBMITTED.v1",
plane: "data",
specversion: "1.0",
source: "/data-plane/supplier-api/prod/submit-mi",
id: "8f2c3b44-4e65-5b1b-a678-1f0bf3d4d222",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
},
"datacontenttype": "application/json",
"dataschema": "https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.ACCEPTED.0.1.0.schema.json",
"dataschemaversion": "1.0.0",
"id": "23f1f09c-a555-4d9b-8405-0b33490bc920",
"plane": "data",
"recordedtime": "2025-08-28T08:45:00.000Z",
"severitynumber": 2,
"severitytext": "INFO",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
},
"datacontenttype": "application/json",
"dataschema": "https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.ACCEPTED.1.0.0.schema.json",
"dataschemaversion": "1.0.0",
"id": "23f1f09c-a555-4d9b-8405-0b33490bc920",
"plane": "data",
"recordedtime": "2025-08-28T08:45:00.000Z",
"severitynumber": 2,
"severitytext": "INFO",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
},
"datacontenttype": "application/json",
"dataschema": "https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.ACCEPTED.1.0.0.schema.json",
"dataschemaversion": "1.0.0",
"id": "23f1f09c-a555-4d9b-8405-0b33490bc920",
"plane": "data",
"recordedtime": "2025-08-28T08:45:00.000Z",
"severitynumber": 2,
"severitytext": "INFO",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
},
"datacontenttype": "application/json",
"dataschema": "https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.FORWARDED.1.0.0.schema.json",
"dataschemaversion": "1.0.0",
"id": "23f1f09c-a555-4d9b-8405-0b33490bc920",
"plane": "data",
"recordedtime": "2025-08-28T08:45:00.000Z",
"severitynumber": 2,
"severitytext": "INFO",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
},
"datacontenttype": "application/json",
"dataschema": "https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.RETURNED.1.0.0.schema.json",
"dataschemaversion": "1.0.0",
"id": "23f1f09c-a555-4d9b-8405-0b33490bc920",
"plane": "data",
"recordedtime": "2025-08-28T08:45:00.000Z",
"severitynumber": 2,
"severitytext": "INFO",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
},
"datacontenttype": "application/json",
"dataschema": "https://notify.nhs.uk/cloudevents/schemas/supplier-api/mi.SUBMITTED.1.0.0.schema.json",
"dataschemaversion": "1.0.0",
"id": "9a3d4c55-5f76-6c2c-b789-2f1cf4e5e333",
"plane": "data",
"recordedtime": "2025-11-16T15:00:00.250Z",
"severitynumber": 2,
"severitytext": "INFO",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
},
"datacontenttype": "application/json",
"dataschema": "https://notify.nhs.uk/cloudevents/schemas/supplier-api/mi.SUBMITTED.2.0.0.schema.json",
"dataschemaversion": "1.0.0",
"id": "8f2c3b44-4e65-5b1b-a678-1f0bf3d4d222",
"plane": "data",
"recordedtime": "2025-11-16T10:30:00.250Z",
"severitynumber": 2,
"severitytext": "INFO",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
},
"datacontenttype": "application/json",
"dataschema": "https://notify.nhs.uk/cloudevents/schemas/supplier-api/mi.SUBMITTED.1.0.0.schema.json",
"dataschemaversion": "1.0.0",
"id": "8f2c3b44-4e65-5b1b-a678-1f0bf3d4d222",
"plane": "data",
"recordedtime": "2025-11-16T10:30:00.250Z",
"severitynumber": 2,
"severitytext": "INFO",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
},
"datacontenttype": "application/json",
"dataschema": "https://notify.nhs.uk/cloudevents/schemas/supplier-api/mi.SUBMITTED.1.0.0.schema.json",
"dataschemaversion": "1.0.0",
"id": "8f2c3b44-4e65-5b1b-a678-1f0bf3d4d222",
"plane": "data",
"recordedtime": "2025-11-16T10:30:00.250Z",
"severitynumber": 2,
"severitytext": "INFO",
Expand Down
29 changes: 21 additions & 8 deletions internal/events/src/events/event-envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export function EventEnvelope<TData extends z.ZodTypeAny>(
examples: typeStrings,
}),

plane: z.literal("data").meta({
title: "plane",
description: "The event bus that this event will be published to",
examples: ["data"],
}),

dataschema: z
.string()
.regex(
Expand All @@ -64,6 +70,15 @@ export function EventEnvelope<TData extends z.ZodTypeAny>(
examples: schemaExamples,
}),

dataschemaversion: z
.string()
.regex(/^1\.\d+\.\d+$/)
.meta({
title: "Data Schema URI",
description: `Version of the schema that describes the event data\n\nMust match the version in dataschema`,
examples: ["1.0.0"],
}),

source: z
.string()
.regex(/^\/data-plane\/supplier-api(?:\/.*)?$/)
Expand Down Expand Up @@ -94,14 +109,12 @@ export function EventEnvelope<TData extends z.ZodTypeAny>(
examples: ["2025-10-01T10:15:30.000Z"],
}),

datacontenttype: z.optional(
z.literal("application/json").meta({
title: "Data Content Type",
description:
"Media type for the data field (fixed to application/json).",
examples: ["application/json"],
}),
),
datacontenttype: z.literal("application/json").meta({
title: "Data Content Type",
description:
"Media type for the data field (fixed to application/json).",
examples: ["application/json"],
}),

traceparent: z
.string()
Expand Down
6 changes: 3 additions & 3 deletions internal/events/src/events/letter-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const $LetterEvent = EventEnvelope(
"letter",
"letter",
$Letter,
$LetterStatus.options,
$LetterStatus.options.map((status) => status.toLowerCase()),
"letter-origin",
).meta({
title: `letter.* Event`,
Expand All @@ -27,13 +27,13 @@ export type LetterEvent = z.infer<typeof $LetterEvent>;
*/
const eventSchema = (status: LetterStatus) =>
EventEnvelope(
`letter.${status}`,
`letter.${status.toLowerCase()}`,
"letter",
$Letter,
[status],
"letter-origin",
).meta({
title: `letter.${status} Event`,
title: `letter.${status.toLowerCase()} Event`,
description: `Event schema for letter status change to ${status}`,
});

Expand Down
30 changes: 15 additions & 15 deletions lambdas/letter-updates-transformer/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Config } from 'jest';
import type { Config } from "jest";

export const baseJestConfig: Config = {
preset: 'ts-jest',
preset: "ts-jest",

// Automatically clear mock calls, instances, contexts and results before every test
clearMocks: true,
Expand All @@ -10,10 +10,10 @@ export const baseJestConfig: Config = {
collectCoverage: true,

// The directory where Jest should output its coverage files
coverageDirectory: './.reports/unit/coverage',
coverageDirectory: "./.reports/unit/coverage",

// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'babel',
coverageProvider: "babel",

coverageThreshold: {
global: {
Expand All @@ -24,36 +24,36 @@ export const baseJestConfig: Config = {
},
},

coveragePathIgnorePatterns: ['/__tests__/'],
transform: { '^.+\\.ts$': 'ts-jest' },
testPathIgnorePatterns: ['.build'],
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
coveragePathIgnorePatterns: ["/__tests__/"],
transform: { "^.+\\.ts$": "ts-jest" },
testPathIgnorePatterns: [".build"],
testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],

// Use this configuration option to add custom reporters to Jest
reporters: [
'default',
"default",
[
'jest-html-reporter',
"jest-html-reporter",
{
pageTitle: 'Test Report',
outputPath: './.reports/unit/test-report.html',
pageTitle: "Test Report",
outputPath: "./.reports/unit/test-report.html",
includeFailureMsg: true,
},
],
],

// The test environment that will be used for testing
testEnvironment: 'jsdom',
testEnvironment: "jsdom",
};

const utilsJestConfig = {
...baseJestConfig,

testEnvironment: 'node',
testEnvironment: "node",

coveragePathIgnorePatterns: [
...(baseJestConfig.coveragePathIgnorePatterns ?? []),
'zod-validators.ts',
"zod-validators.ts",
],
};

Expand Down
8 changes: 7 additions & 1 deletion lambdas/letter-updates-transformer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{
"dependencies": {
"esbuild": "^0.24.0"
"@aws-sdk/client-sns": "^3.943.0",
"@aws-sdk/util-dynamodb": "^3.943.0",
"@internal/datastore": "^0.1.0",
"@nhsdigital/nhs-notify-event-schemas-supplier-api": "*",
"aws-lambda": "^1.0.7",
"esbuild": "^0.24.0",
"pino": "^10.1.0"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
Expand Down
Loading
Loading