diff --git a/infrastructure/terraform/components/dl/README.md b/infrastructure/terraform/components/dl/README.md index 964704f0..d15e7b28 100644 --- a/infrastructure/terraform/components/dl/README.md +++ b/infrastructure/terraform/components/dl/README.md @@ -31,6 +31,7 @@ No requirements. | [parent\_acct\_environment](#input\_parent\_acct\_environment) | Name of the environment responsible for the acct resources used, affects things like DNS zone. Useful for named dev environments | `string` | `"main"` | no | | [pdm\_mock\_access\_token](#input\_pdm\_mock\_access\_token) | Mock access token for PDM API authentication (used in local/dev environments) | `string` | `"mock-pdm-token"` | no | | [pdm\_use\_non\_mock\_token](#input\_pdm\_use\_non\_mock\_token) | Whether to use the shared APIM access token from SSM (/component/environment/apim/access\_token) instead of the mock token | `bool` | `false` | no | +| [pii\_data\_retention\_policy\_days](#input\_pii\_data\_retention\_policy\_days) | The number of days for data retention policy for PII | `number` | `534` | no | | [project](#input\_project) | The name of the tfscaffold project | `string` | n/a | yes | | [queue\_batch\_size](#input\_queue\_batch\_size) | maximum number of queue items to process | `number` | `10` | no | | [queue\_batch\_window\_seconds](#input\_queue\_batch\_window\_seconds) | maximum time in seconds between processing events | `number` | `1` | no | @@ -53,11 +54,13 @@ No requirements. | [pdm\_uploader](#module\_pdm\_uploader) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [print\_analyser](#module\_print\_analyser) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [print\_status\_handler](#module\_print\_status\_handler) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | +| [report\_event\_transformer](#module\_report\_event\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [s3bucket\_cf\_logs](#module\_s3bucket\_cf\_logs) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a | | [s3bucket\_file\_safe](#module\_s3bucket\_file\_safe) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a | | [s3bucket\_letters](#module\_s3bucket\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a | | [s3bucket\_non\_pii\_data](#module\_s3bucket\_non\_pii\_data) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a | | [s3bucket\_pii\_data](#module\_s3bucket\_pii\_data) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a | +| [s3bucket\_reporting](#module\_s3bucket\_reporting) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a | | [s3bucket\_static\_assets](#module\_s3bucket\_static\_assets) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a | | [sqs\_core\_notifier](#module\_sqs\_core\_notifier) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a | | [sqs\_event\_publisher\_errors](#module\_sqs\_event\_publisher\_errors) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a | diff --git a/infrastructure/terraform/components/dl/athena_workgroup_reporting.tf b/infrastructure/terraform/components/dl/athena_workgroup_reporting.tf new file mode 100644 index 00000000..6529f19f --- /dev/null +++ b/infrastructure/terraform/components/dl/athena_workgroup_reporting.tf @@ -0,0 +1,19 @@ +resource "aws_athena_workgroup" "reporting" { + name = local.csi + description = "Athena Workgroup for ${var.environment}" + force_destroy = true + + configuration { + enforce_workgroup_configuration = true + + result_configuration { + expected_bucket_owner = var.aws_account_id + output_location = "s3://${module.s3bucket_reporting.bucket}/athena-output/" + + encryption_configuration { + encryption_option = "SSE_KMS" + kms_key_arn = module.kms.key_arn + } + } + } +} diff --git a/infrastructure/terraform/components/dl/aws_iam_policy_document_eventbridge_firehose_assume_role.tf b/infrastructure/terraform/components/dl/aws_iam_policy_document_eventbridge_firehose_assume_role.tf new file mode 100644 index 00000000..296905a7 --- /dev/null +++ b/infrastructure/terraform/components/dl/aws_iam_policy_document_eventbridge_firehose_assume_role.tf @@ -0,0 +1,40 @@ +# IAM role for EventBridge to write to Kinesis Firehose +data "aws_iam_policy_document" "eventbridge_firehose_assume_role" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["events.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "eventbridge_firehose" { + name = "${local.csi}-eventbridge-firehose" + description = "Role for EventBridge to write to Kinesis Firehose" + assume_role_policy = data.aws_iam_policy_document.eventbridge_firehose_assume_role.json +} + +data "aws_iam_policy_document" "eventbridge_firehose_policy" { + statement { + effect = "Allow" + + actions = [ + "firehose:PutRecord", + "firehose:PutRecordBatch" + ] + + resources = [ + aws_kinesis_firehose_delivery_stream.to_s3_reporting.arn + ] + } +} + +resource "aws_iam_role_policy" "eventbridge_firehose" { + name = "${local.csi}-eventbridge-firehose" + role = aws_iam_role.eventbridge_firehose.id + policy = data.aws_iam_policy_document.eventbridge_firehose_policy.json +} diff --git a/infrastructure/terraform/components/dl/cloudwatch_event_rule_all_events.tf b/infrastructure/terraform/components/dl/cloudwatch_event_rule_all_events.tf new file mode 100644 index 00000000..f27e48de --- /dev/null +++ b/infrastructure/terraform/components/dl/cloudwatch_event_rule_all_events.tf @@ -0,0 +1,20 @@ +resource "aws_cloudwatch_event_rule" "all_events" { + name = "${local.csi}-all-events" + description = "Event rule to match all Digital Letters events" + event_bus_name = aws_cloudwatch_event_bus.main.name + + event_pattern = jsonencode({ + "detail" : { + "type" : [{ + "prefix" : "uk.nhs.notify.digital.letters." + }] + } + }) +} + +resource "aws_cloudwatch_event_target" "reporting_firehose" { + rule = aws_cloudwatch_event_rule.all_events.name + arn = aws_kinesis_firehose_delivery_stream.to_s3_reporting.arn + role_arn = aws_iam_role.eventbridge_firehose.arn + event_bus_name = aws_cloudwatch_event_bus.main.name +} diff --git a/infrastructure/terraform/components/dl/cloudwatch_log_group_kinesis_logs.tf b/infrastructure/terraform/components/dl/cloudwatch_log_group_kinesis_logs.tf new file mode 100644 index 00000000..4133622f --- /dev/null +++ b/infrastructure/terraform/components/dl/cloudwatch_log_group_kinesis_logs.tf @@ -0,0 +1,9 @@ +resource "aws_cloudwatch_log_group" "kinesis_logs" { + name = "/aws/kinesisfirehose/${local.csi}-to-s3-reporting" + retention_in_days = var.log_retention_in_days +} + +resource "aws_cloudwatch_log_stream" "reporting_kinesis_logs" { + name = "${local.csi}reportingKinesisLogs" + log_group_name = aws_cloudwatch_log_group.kinesis_logs.name +} diff --git a/infrastructure/terraform/components/dl/glue_catalog_database_reporting.tf b/infrastructure/terraform/components/dl/glue_catalog_database_reporting.tf new file mode 100644 index 00000000..34b7c8b0 --- /dev/null +++ b/infrastructure/terraform/components/dl/glue_catalog_database_reporting.tf @@ -0,0 +1,4 @@ +resource "aws_glue_catalog_database" "reporting" { + name = "${local.csi}-reporting" + description = "Reporting database for ${var.environment}" +} diff --git a/infrastructure/terraform/components/dl/glue_catalog_table_event_record.tf b/infrastructure/terraform/components/dl/glue_catalog_table_event_record.tf new file mode 100644 index 00000000..f03c9586 --- /dev/null +++ b/infrastructure/terraform/components/dl/glue_catalog_table_event_record.tf @@ -0,0 +1,80 @@ +resource "aws_glue_catalog_table" "event_record" { + name = "event_record" + description = "Event records for ${var.environment}" + database_name = aws_glue_catalog_database.reporting.name + + table_type = "EXTERNAL_TABLE" + + storage_descriptor { + location = "s3://${module.s3bucket_reporting.bucket}/${local.firehose_output_path_prefix}/reporting/parquet/event_record" + + input_format = "org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat" + output_format = "org.apache.hadoop.hive.ql.io.parquet.MapredParquetOutputFormat" + + ser_de_info { + serialization_library = "org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe" + } + + # additional columns must be added at the end of the list + columns { + name = "messagereference" + type = "string" + } + columns { + name = "pagecount" + type = "int" + } + columns { + name = "supplierid" + type = "string" + } + columns { + name = "time" + type = "string" + } + columns { + name = "type" + type = "string" + } + } + + partition_keys { + name = "senderid" + type = "string" + } + + partition_keys { + name = "__year" + type = "int" + } + partition_keys { + name = "__month" + type = "int" + } + partition_keys { + name = "__day" + type = "int" + } + + parameters = { + EXTERNAL = "TRUE" + "parquet.compression" = "SNAPPY" + compressionType = "none" + classification = "parquet" + } +} + +resource "aws_glue_partition_index" "event_record" { + database_name = aws_glue_catalog_database.reporting.name + table_name = aws_glue_catalog_table.event_record.name + + partition_index { + index_name = "data" + keys = ["senderid", "__year", "__month", "__day"] + } + + timeouts { + create = "60m" + delete = "60m" + } +} diff --git a/infrastructure/terraform/components/dl/kinesis_firehose_delivery_stream_to_s3_reporting.tf b/infrastructure/terraform/components/dl/kinesis_firehose_delivery_stream_to_s3_reporting.tf new file mode 100644 index 00000000..86efee7f --- /dev/null +++ b/infrastructure/terraform/components/dl/kinesis_firehose_delivery_stream_to_s3_reporting.tf @@ -0,0 +1,228 @@ +resource "aws_kinesis_firehose_delivery_stream" "to_s3_reporting" { + name = "${local.csi}-to-s3-reporting" + + destination = "extended_s3" + + extended_s3_configuration { + role_arn = aws_iam_role.firehose_role.arn + bucket_arn = module.s3bucket_reporting.arn + + prefix = "${local.firehose_output_path_prefix}/reporting/parquet/${aws_glue_catalog_table.event_record.name}/senderid=!{partitionKeyFromLambda:senderId}/__year=!{partitionKeyFromLambda:year}/__month=!{partitionKeyFromLambda:month}/__day=!{partitionKeyFromLambda:day}/" + error_output_prefix = "${local.firehose_output_path_prefix}/errors/!{timestamp:yyyy}-!{timestamp:MM}-!{timestamp:dd}-!{timestamp:HH}/!{firehose:error-output-type}/" + + buffering_size = 128 + buffering_interval = 300 + + dynamic_partitioning_configuration { + enabled = true + } + + processing_configuration { + enabled = "true" + + processors { + type = "Lambda" + + parameters { + parameter_name = "LambdaArn" + parameter_value = "${module.report_event_transformer.function_arn}:$LATEST" + } + parameters { + parameter_name = "RoleArn" + parameter_value = aws_iam_role.firehose_role.arn + } + parameters { + parameter_name = "BufferSizeInMBs" + parameter_value = 1 + } + parameters { + parameter_name = "BufferIntervalInSeconds" + parameter_value = 301 + } + } + } + + data_format_conversion_configuration { + input_format_configuration { + deserializer { + open_x_json_ser_de {} + } + } + + output_format_configuration { + serializer { + parquet_ser_de {} + } + } + + schema_configuration { + database_name = aws_glue_catalog_database.reporting.name + role_arn = aws_iam_role.firehose_role.arn + table_name = aws_glue_catalog_table.event_record.name + } + } + + cloudwatch_logging_options { + enabled = true + log_group_name = aws_cloudwatch_log_group.kinesis_logs.name + log_stream_name = aws_cloudwatch_log_stream.reporting_kinesis_logs.name + } + } + +} + +resource "aws_iam_role" "firehose_role" { + name = "${local.csi}-firehose" + description = "Firehose Role" + assume_role_policy = data.aws_iam_policy_document.firehose_assume_role.json +} + +data "aws_iam_policy_document" "firehose_assume_role" { + statement { + sid = "FirehoseAssumeRole" + effect = "Allow" + + principals { + type = "Service" + identifiers = [ + "firehose.amazonaws.com" + ] + } + + actions = [ + "sts:AssumeRole" + ] + } +} + +resource "aws_iam_policy" "firehose_policy" { + name = "${local.csi}-firehose" + description = "Firehose Policy" + path = "/" + policy = data.aws_iam_policy_document.firehose_policy.json +} + +resource "aws_iam_role_policy_attachment" "firehose" { + role = aws_iam_role.firehose_role.name + policy_arn = aws_iam_policy.firehose_policy.arn +} + +data "aws_iam_policy_document" "firehose_policy" { + version = "2012-10-17" + + statement { + actions = [ + "logs:PutLogEvents", + ] + + resources = [ + aws_cloudwatch_log_group.kinesis_logs.arn, + aws_cloudwatch_log_stream.reporting_kinesis_logs.arn + ] + + effect = "Allow" + } + + statement { + actions = [ + "lambda:InvokeFunction", + "lambda:GetFunctionConfiguration", + ] + + resources = [ + "${module.report_event_transformer.function_arn}:$LATEST", + ] + } + + statement { + sid = "AllowSSE" + effect = "Allow" + actions = [ + "kms:DescribeKey", + "kms:Encrypt", + "kms:Decrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*", + ] + + resources = [ + module.kms.key_arn + ] + } + + statement { + sid = "DestinationS3Access" + effect = "Allow" + + actions = [ + "s3:AbortMultipartUpload", + "s3:GetBucketLocation", + "s3:GetObject", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + "s3:PutObject", + ] + + resources = [ + module.s3bucket_reporting.arn, + "${module.s3bucket_reporting.arn}/${local.firehose_output_path_prefix}/*" + ] + } + + statement { + sid = "EncryptTargetData" + effect = "Allow" + + actions = [ + "kms:Decrypt", + "kms:GenerateDataKey" + ] + resources = [ + module.kms.key_arn + ] + + condition { + test = "StringLike" + variable = "kms:EncryptionContext:aws:s3:arn" + values = [ + "${module.s3bucket_reporting.arn}" + ] + } + + condition { + test = "StringEquals" + variable = "kms:ViaService" + values = [ + "s3.${var.region}.amazonaws.com" + ] + } + } + + statement { + sid = "AllowListDataStream" + effect = "Allow" + actions = [ + "kinesis:ListStreams" + ] + + resources = [ + "*" + ] + } + + statement { + sid = "AllowGlueTableAccess" + effect = "Allow" + actions = [ + "glue:GetTable", + "glue:GetTableVersion", + "glue:GetTableVersions" + ] + + resources = [ + aws_glue_catalog_table.event_record.arn, + aws_glue_catalog_database.reporting.arn, + "arn:aws:glue:${var.region}:${var.aws_account_id}:catalog" + ] + } +} diff --git a/infrastructure/terraform/components/dl/locals.tf b/infrastructure/terraform/components/dl/locals.tf index da187ad9..c5bbee0b 100644 --- a/infrastructure/terraform/components/dl/locals.tf +++ b/infrastructure/terraform/components/dl/locals.tf @@ -12,4 +12,9 @@ locals { root_domain_id = local.acct.route53_zone_ids["digital-letters"] ttl_shard_count = 3 deploy_pdm_mock = var.enable_pdm_mock + firehose_output_path_prefix = "kinesis-firehose-output" + pii_retention_config = { + current_days = var.pii_data_retention_policy_days, + non_current_days = 14 + } } diff --git a/infrastructure/terraform/components/dl/module_lambda_report_event_transformer.tf b/infrastructure/terraform/components/dl/module_lambda_report_event_transformer.tf new file mode 100644 index 00000000..21121525 --- /dev/null +++ b/infrastructure/terraform/components/dl/module_lambda_report_event_transformer.tf @@ -0,0 +1,32 @@ +module "report_event_transformer" { + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip" + + function_name = "report-event-transformer" + description = "A function for transforming all digital letter events" + + aws_account_id = var.aws_account_id + component = local.component + environment = var.environment + project = var.project + region = var.region + group = var.group + + log_retention_in_days = var.log_retention_in_days + kms_key_arn = module.kms.key_arn + + function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"] + function_code_base_path = local.aws_lambda_functions_dir_path + function_code_dir = "report-event-transformer/dist" + function_include_common = true + handler_function_name = "handler" + runtime = "nodejs22.x" + memory = 128 + timeout = 60 + log_level = var.log_level + + force_lambda_code_deploy = var.force_lambda_code_deploy + enable_lambda_insights = false + + log_destination_arn = local.log_destination_arn + log_subscription_role_arn = local.acct.log_subscription_role_arn +} diff --git a/infrastructure/terraform/components/dl/module_s3bucket_reporting.tf b/infrastructure/terraform/components/dl/module_s3bucket_reporting.tf new file mode 100644 index 00000000..739f7c99 --- /dev/null +++ b/infrastructure/terraform/components/dl/module_s3bucket_reporting.tf @@ -0,0 +1,75 @@ +module "s3bucket_reporting" { + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip" + + name = "reporting" + + aws_account_id = var.aws_account_id + region = var.region + project = var.project + environment = var.environment + component = local.component + + kms_key_arn = module.kms.key_arn + + policy_documents = [data.aws_iam_policy_document.s3bucket_reporting.json] + + force_destroy = var.force_destroy + + lifecycle_rules = [ + { + prefix = "" + enabled = true + + expiration = { + days = local.pii_retention_config.current_days + } + + noncurrent_version_expiration = { + noncurrent_days = local.pii_retention_config.non_current_days + } + } + ] +} + +data "aws_iam_policy_document" "s3bucket_reporting" { + statement { + sid = "AllowManagedAccountsToList" + effect = "Allow" + + actions = [ + "s3:ListBucket", + ] + + resources = [ + module.s3bucket_reporting.arn, + ] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${var.aws_account_id}:root" + ] + } + } + + statement { + sid = "AllowManagedAccountsToGet" + effect = "Allow" + + actions = [ + "s3:GetObject", + "s3:PutObject", + ] + + resources = [ + "${module.s3bucket_reporting.arn}/*", + ] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${var.aws_account_id}:root" + ] + } + } +} diff --git a/infrastructure/terraform/components/dl/variables.tf b/infrastructure/terraform/components/dl/variables.tf index be79f015..fb9b496a 100644 --- a/infrastructure/terraform/components/dl/variables.tf +++ b/infrastructure/terraform/components/dl/variables.tf @@ -89,7 +89,7 @@ variable "parent_acct_environment" { variable "mesh_poll_schedule" { type = string description = "Schedule to poll MESH for messages" - default = "rate(5 minutes)" # Every 5 minutes + default = "rate(5 minutes)" # Every 5 minutes } variable "enable_mock_mesh" { @@ -140,7 +140,6 @@ variable "apim_base_url" { default = "https://int.api.service.nhs.uk" } - variable "core_notify_url" { type = string description = "The URL used to send requests to Notify" @@ -169,6 +168,11 @@ variable "force_destroy" { type = bool description = "Flag to force deletion of S3 buckets" default = false + + validation { + condition = !(var.force_destroy && var.environment == "prod") + error_message = "force_destroy must not be set to true when environment is 'prod'." + } } variable "enable_pdm_mock" { @@ -176,3 +180,9 @@ variable "enable_pdm_mock" { description = "Flag indicating whether to deploy PDM mock API (should be false in production environments)" default = true } + +variable "pii_data_retention_policy_days" { + type = number + description = "The number of days for data retention policy for PII" + default = 534 +} diff --git a/lambdas/report-event-transformer/jest.config.ts b/lambdas/report-event-transformer/jest.config.ts new file mode 100644 index 00000000..c02601ae --- /dev/null +++ b/lambdas/report-event-transformer/jest.config.ts @@ -0,0 +1,5 @@ +import { baseJestConfig } from '../../jest.config.base'; + +const config = baseJestConfig; + +export default config; diff --git a/lambdas/report-event-transformer/package.json b/lambdas/report-event-transformer/package.json new file mode 100644 index 00000000..a6d9f388 --- /dev/null +++ b/lambdas/report-event-transformer/package.json @@ -0,0 +1,25 @@ +{ + "dependencies": { + "aws-lambda": "^1.0.7", + "utils": "^0.0.1", + "zod": "^4.1.12" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/aws-lambda": "^8.10.155", + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "jest-mock-extended": "^3.0.7", + "typescript": "^5.9.3" + }, + "name": "nhs-notify-digital-letters-report-event-transformer", + "private": true, + "scripts": { + "lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test:unit": "jest", + "typecheck": "tsc --noEmit" + }, + "version": "0.0.1" +} diff --git a/lambdas/report-event-transformer/src/__tests__/apis/firehose-handler.test.ts b/lambdas/report-event-transformer/src/__tests__/apis/firehose-handler.test.ts new file mode 100644 index 00000000..10942086 --- /dev/null +++ b/lambdas/report-event-transformer/src/__tests__/apis/firehose-handler.test.ts @@ -0,0 +1,112 @@ +import { mock } from 'jest-mock-extended'; +import { createHandler } from 'apis/firehose-handler'; +import { Logger } from 'utils'; +import { digitalLettersEvent, firehoseEvent } from '__tests__/test-data'; + +const logger = mock(); + +const handler = createHandler({ + logger, +}); + +describe('Firehose Handler', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('status', () => { + it('should process valid digital letters event and return report event', async () => { + const response = await handler(firehoseEvent([digitalLettersEvent])); + + expect(logger.info).toHaveBeenCalledWith( + 'Received Firehose Event of 1 record(s)', + ); + + expect(logger.info).toHaveBeenCalledWith( + '1 of 1 records processed successfully', + ); + + expect(response.records).toHaveLength(1); + expect(response.records[0]).toMatchObject({ + recordId: '1', + result: 'Ok', + metadata: { + partitionKeys: { + year: '2023', + month: '6', + day: '20', + senderId: 'sender1', + }, + }, + }); + + // Verify the data is base64 encoded JSON + const decodedData = JSON.parse( + Buffer.from(response.records[0].data!, 'base64').toString('utf8'), + ); + expect(decodedData).toEqual({ + messageReference: digitalLettersEvent.data.messageReference, + senderId: digitalLettersEvent.data.senderId, + pageCount: digitalLettersEvent.data.pageCount, + supplierId: digitalLettersEvent.data.supplierId, + time: digitalLettersEvent.time, + type: digitalLettersEvent.type, + }); + }); + }); + + describe('errors', () => { + it('should return ProcessingFailed for records with invalid JSON', async () => { + const event = firehoseEvent([digitalLettersEvent]); + event.records[0].data = Buffer.from('not-json').toString('base64'); + + const result = await handler(event); + + expect(logger.warn).toHaveBeenCalledWith({ + err: expect.any(SyntaxError), + description: 'Error parsing firehose record', + }); + + expect(logger.info).toHaveBeenCalledWith( + '0 of 1 records processed successfully', + ); + + expect(result.records).toHaveLength(1); + expect(result.records[0]).toMatchObject({ + recordId: '1', + result: 'ProcessingFailed', + }); + }); + + it('should return ProcessingFailed for records with invalid event schema', async () => { + const invalidEvent = { + ...digitalLettersEvent, + type: 123, // Invalid: should be string + }; + const event = firehoseEvent([invalidEvent as any]); + + const result = await handler(event); + + expect(logger.warn).toHaveBeenCalledWith({ + err: expect.objectContaining({ + issues: expect.arrayContaining([ + expect.objectContaining({ + path: ['type'], + }), + ]), + }), + description: 'Error parsing firehose item', + }); + + expect(logger.info).toHaveBeenCalledWith( + '0 of 1 records processed successfully', + ); + + expect(result.records).toHaveLength(1); + expect(result.records[0]).toMatchObject({ + recordId: '1', + result: 'ProcessingFailed', + }); + }); + }); +}); diff --git a/lambdas/report-event-transformer/src/__tests__/container.test.ts b/lambdas/report-event-transformer/src/__tests__/container.test.ts new file mode 100644 index 00000000..1859a92a --- /dev/null +++ b/lambdas/report-event-transformer/src/__tests__/container.test.ts @@ -0,0 +1,12 @@ +import { createContainer } from 'container'; + +jest.mock('utils', () => ({ + logger: {}, +})); + +describe('container', () => { + it('should create container', () => { + const container = createContainer(); + expect(container).toBeDefined(); + }); +}); diff --git a/lambdas/report-event-transformer/src/__tests__/index.test.ts b/lambdas/report-event-transformer/src/__tests__/index.test.ts new file mode 100644 index 00000000..c1ecd810 --- /dev/null +++ b/lambdas/report-event-transformer/src/__tests__/index.test.ts @@ -0,0 +1,15 @@ +import { handler } from 'index'; + +jest.mock('apis/firehose-handler', () => ({ + createHandler: jest.fn(() => jest.fn()), +})); + +jest.mock('container', () => ({ + createContainer: jest.fn(() => ({})), +})); + +describe('index', () => { + it('should export handler', () => { + expect(handler).toBeDefined(); + }); +}); diff --git a/lambdas/report-event-transformer/src/__tests__/test-data.ts b/lambdas/report-event-transformer/src/__tests__/test-data.ts new file mode 100644 index 00000000..e8043885 --- /dev/null +++ b/lambdas/report-event-transformer/src/__tests__/test-data.ts @@ -0,0 +1,52 @@ +import { FirehoseTransformationEvent } from 'aws-lambda'; +import { DigitalLettersEvent } from 'types/events'; + +const baseEvent = { + id: '550e8400-e29b-41d4-a716-446655440001', + specversion: '1.0', + source: + '/nhs/england/notify/production/primary/data-plane/digitalletters/pdm', + subject: + 'customer/920fca11-596a-4eca-9c47-99f624614658/recipient/769acdd4-6a47-496f-999f-76a6fd2c3959', + type: 'uk.nhs.notify.digital.letters.pdm.resource.submitted.v1', + time: '2023-06-20T12:00:00Z', + recordedtime: '2023-06-20T12:00:00.250Z', + severitynumber: 2, + traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01', + datacontenttype: 'application/json', + dataschema: + 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10/digital-letter-base-data.schema.json', + severitytext: 'INFO', + data: { + resourceId: 'a2bcbb42-ab7e-42b6-88d6-74f8d3ca4a09', + messageReference: 'ref1', + senderId: 'sender1', + }, +}; + +export const digitalLettersEvent = { + ...baseEvent, + type: 'uk.nhs.notify.digital.letters.pdm.resource.submitted.v1', + dataschema: + 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-pdm-resource-submitted-data.schema.json', +} as DigitalLettersEvent; + +export const firehoseEvent = ( + events: DigitalLettersEvent[], +): FirehoseTransformationEvent => ({ + invocationId: 'test-invocation-id', + deliveryStreamArn: + 'arn:aws:firehose:eu-west-2:123456789012:deliverystream/test', + region: 'eu-west-2', + records: events.map((event, i) => ({ + recordId: String(i + 1), + approximateArrivalTimestamp: Date.now(), + data: Buffer.from( + JSON.stringify({ + version: '0', + id: 'ab07d406-0797-e919-ff9b-3ad9c5498114', + detail: event, + }), + ).toString('base64'), + })), +}); diff --git a/lambdas/report-event-transformer/src/apis/firehose-handler.ts b/lambdas/report-event-transformer/src/apis/firehose-handler.ts new file mode 100644 index 00000000..171003a2 --- /dev/null +++ b/lambdas/report-event-transformer/src/apis/firehose-handler.ts @@ -0,0 +1,113 @@ +import type { + FirehoseTransformationEvent, + FirehoseTransformationEventRecord, + FirehoseTransformationResult, + FirehoseTransformationResultRecord, +} from 'aws-lambda'; +import { + $DigitalLettersEvent, + DigitalLettersEvent, + FlatDigitalLettersEvent, + ReportEvent, +} from 'types/events'; +import { Logger } from 'utils'; + +export interface HandlerDependencies { + logger: Logger; +} + +type ValidatedRecord = { + recordId: string; + event: DigitalLettersEvent; +}; + +function validateRecord( + { data, recordId }: FirehoseTransformationEventRecord, + logger: Logger, +): ValidatedRecord | null { + try { + const eventBody = JSON.parse(Buffer.from(data, 'base64').toString('utf8')); + const eventDetail = eventBody.detail; + + const { + data: item, + error: parseError, + success: parseSuccess, + } = $DigitalLettersEvent.safeParse(eventDetail); + + if (!parseSuccess) { + logger.warn({ + err: parseError, + description: 'Error parsing firehose item', + }); + + return null; + } + + return { recordId, event: item }; + } catch (error) { + logger.warn({ + err: error, + description: 'Error parsing firehose record', + }); + + return null; + } +} + +function generateReportEvent(validatedRecord: ValidatedRecord): ReportEvent { + const { messageReference, pageCount, senderId, supplierId } = + validatedRecord.event.data; + const { time, type } = validatedRecord.event; + const eventTime = new Date(time); + + const flattenedEvent: FlatDigitalLettersEvent = { + messageReference, + senderId, + pageCount, + supplierId, + time, + type, + }; + + return { + recordId: validatedRecord.recordId, + data: Buffer.from(JSON.stringify(flattenedEvent)).toString('base64'), + result: 'Ok', + metadata: { + partitionKeys: { + year: eventTime.getUTCFullYear().toString(), + month: (eventTime.getUTCMonth() + 1).toString(), + day: eventTime.getUTCDate().toString(), + senderId, + }, + }, + }; +} + +export const createHandler = ({ logger }: HandlerDependencies) => + async function handler( + firehoseTransformationEvent: FirehoseTransformationEvent, + ): Promise { + const receivedItemCount = firehoseTransformationEvent.records.length; + const failedEvents: FirehoseTransformationResultRecord[] = []; + const validEvents: ReportEvent[] = []; + + logger.info(`Received Firehose Event of ${receivedItemCount} record(s)`); + + for (const record of firehoseTransformationEvent.records) { + const validated = validateRecord(record, logger); + if (validated) { + validEvents.push(generateReportEvent(validated)); + } else { + failedEvents.push({ ...record, result: 'ProcessingFailed' }); + } + } + + const processedItemCount = receivedItemCount - failedEvents.length; + logger.info( + `${processedItemCount} of ${receivedItemCount} records processed successfully`, + ); + + return { records: [...validEvents, ...failedEvents] }; + }; diff --git a/lambdas/report-event-transformer/src/container.ts b/lambdas/report-event-transformer/src/container.ts new file mode 100644 index 00000000..431346a9 --- /dev/null +++ b/lambdas/report-event-transformer/src/container.ts @@ -0,0 +1,8 @@ +import { HandlerDependencies } from 'apis/firehose-handler'; +import { logger } from 'utils'; + +export const createContainer = (): HandlerDependencies => { + return { logger }; +}; + +export default createContainer; diff --git a/lambdas/report-event-transformer/src/index.ts b/lambdas/report-event-transformer/src/index.ts new file mode 100644 index 00000000..0794289c --- /dev/null +++ b/lambdas/report-event-transformer/src/index.ts @@ -0,0 +1,6 @@ +import { createHandler } from 'apis/firehose-handler'; +import { createContainer } from 'container'; + +export const handler = createHandler(createContainer()); + +export default handler; diff --git a/lambdas/report-event-transformer/src/types/events.ts b/lambdas/report-event-transformer/src/types/events.ts new file mode 100644 index 00000000..c018f0f2 --- /dev/null +++ b/lambdas/report-event-transformer/src/types/events.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; + +export const $DigitalLettersEvent = z.object({ + data: z.object({ + messageReference: z.string(), + senderId: z.string(), + pageCount: z.number().optional(), + supplierId: z.string().optional(), + }), + time: z.string(), + type: z.string(), +}); + +export type DigitalLettersEvent = z.infer; + +export type FlatDigitalLettersEvent = { + messageReference: string; + senderId: string; + pageCount?: number; + supplierId?: string; + time: string; + type: string; +}; + +// Custom type similar to FirehoseTransformationResultRecord from aws-lambda, +// but with strict metadata typing for dynamic partitioning keys +export type ReportEvent = { + recordId: string; + data: string; + result: 'Ok' | 'Dropped' | 'ProcessingFailed'; + metadata: { + partitionKeys: { + year: string; + month: string; + day: string; + senderId: string; + }; + }; +}; diff --git a/lambdas/report-event-transformer/tsconfig.json b/lambdas/report-event-transformer/tsconfig.json new file mode 100644 index 00000000..f7bcaa1f --- /dev/null +++ b/lambdas/report-event-transformer/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "baseUrl": "./src/", + "isolatedModules": true + }, + "extends": "@tsconfig/node22/tsconfig.json", + "include": [ + "src/**/*", + "jest.config.ts" + ] +} diff --git a/package-lock.json b/package-lock.json index 421176ad..7b16ce83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "lambdas/core-notifier-lambda", "lambdas/print-status-handler", "lambdas/print-analyser", + "lambdas/report-event-transformer", "utils/utils", "utils/sender-management", "src/cloudevents", @@ -2723,6 +2724,329 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "lambdas/report-event-transformer": { + "name": "nhs-notify-digital-letters-report-event-transformer", + "version": "0.0.1", + "dependencies": { + "aws-lambda": "^1.0.7", + "utils": "^0.0.1", + "zod": "^4.1.12" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/aws-lambda": "^8.10.155", + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "jest-mock-extended": "^3.0.7", + "typescript": "^5.9.3" + } + }, + "lambdas/report-event-transformer/node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "lambdas/report-event-transformer/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "lambdas/report-event-transformer/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "lambdas/report-event-transformer/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "lambdas/report-event-transformer/node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "lambdas/report-event-transformer/node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "lambdas/report-event-transformer/node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "lambdas/report-event-transformer/node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "lambdas/report-event-transformer/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "lambdas/report-event-transformer/node_modules/jest-mock-extended": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-3.0.7.tgz", + "integrity": "sha512-7lsKdLFcW9B9l5NzZ66S/yTQ9k8rFtnwYdCNuRU/81fqDWicNDVhitTSPnrGmNeNm0xyw0JHexEOShrIKRCIRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-essentials": "^10.0.0" + }, + "peerDependencies": { + "jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0", + "typescript": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "lambdas/report-event-transformer/node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "lambdas/report-event-transformer/node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "lambdas/report-event-transformer/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "lambdas/report-event-transformer/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "lambdas/report-event-transformer/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "lambdas/report-event-transformer/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "lambdas/ttl-create-lambda": { "name": "nhs-notify-digital-letters-ttl-create-lambda", "version": "0.0.1", @@ -11389,6 +11713,97 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -18671,6 +19086,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -19170,6 +19595,10 @@ "resolved": "lambdas/print-status-handler", "link": true }, + "node_modules/nhs-notify-digital-letters-report-event-transformer": { + "resolved": "lambdas/report-event-transformer", + "link": true + }, "node_modules/nhs-notify-digital-letters-ttl-create-lambda": { "resolved": "lambdas/ttl-create-lambda", "link": true @@ -19940,6 +20369,20 @@ "node": ">= 0.6.0" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -20669,6 +21112,13 @@ "node": ">=0.3.1" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index 14477577..a7a6e0e1 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "lambdas/core-notifier-lambda", "lambdas/print-status-handler", "lambdas/print-analyser", + "lambdas/report-event-transformer", "utils/utils", "utils/sender-management", "src/cloudevents", diff --git a/project.code-workspace b/project.code-workspace index b7407a2a..16295b33 100644 --- a/project.code-workspace +++ b/project.code-workspace @@ -80,15 +80,22 @@ "terminal.integrated.scrollback": 10000, "shellcheck.run": "onSave", "jest.virtualFolders": [ + { "name": "core-notifier-lambda", "rootPath": "lambdas/core-notifier-lambda" }, { "name": "key-generation", "rootPath": "lambdas/key-generation" }, - { "name": "refresh-apim-access-token", "rootPath": "lambdas/refresh-apim-access-token" }, + { "name": "pdm-mock-lambda", "rootPath": "lambdas/pdm-mock-lambda" }, + { "name": "pdm-poll-lambda", "rootPath": "lambdas/pdm-poll-lambda" }, + { "name": "pdm-uploader-lambda", "rootPath": "lambdas/pdm-uploader-lambda" }, + { "name": "print-status-handler", "rootPath": "lambdas/print-status-handler" }, { "name": "python-schema-generator", "rootPath": "src/python-schema-generator" }, + { "name": "refresh-apim-access-token", "rootPath": "lambdas/refresh-apim-access-token" }, + { "name": "report-event-transformer", "rootPath": "lambdas/report-event-transformer" }, + { "name": "sender-management", "rootPath": "utils/sender-management" }, { "name": "ttl-create-lambda", "rootPath": "lambdas/ttl-create-lambda/" }, { "name": "ttl-handle-expiry-lambda", "rootPath": "lambdas/ttl-handle-expiry-lambda" }, { "name": "ttl-poll-lambda", "rootPath": "lambdas/ttl-poll-lambda" }, - { "name": "sender-management", "rootPath": "utils/sender-management" }, { "name": "utils", "rootPath": "utils/utils" }, ], + "testing.defaultGutterClickAction": "runWithCoverage", }, "extensions": { "recommendations": [