diff --git a/infrastructure/terraform/components/dl/README.md b/infrastructure/terraform/components/dl/README.md
index 343559c8..e6f1f603 100644
--- a/infrastructure/terraform/components/dl/README.md
+++ b/infrastructure/terraform/components/dl/README.md
@@ -13,6 +13,8 @@ No requirements.
| [apim\_auth\_token\_url](#input\_apim\_auth\_token\_url) | URL to generate an APIM auth token | `string` | `"https://int.api.service.nhs.uk/oauth2/token"` | no |
| [apim\_base\_url](#input\_apim\_base\_url) | The URL used to send requests to PDM | `string` | `"https://int.api.service.nhs.uk"` | no |
| [apim\_keygen\_schedule](#input\_apim\_keygen\_schedule) | Schedule to refresh key pairs if necessary | `string` | `"cron(0 14 * * ? *)"` | no |
+| [athena\_query\_max\_polling\_attemps](#input\_athena\_query\_max\_polling\_attemps) | The number of times athena will be polled to check if a query is completed | `number` | `50` | no |
+| [athena\_query\_polling\_time\_seconds](#input\_athena\_query\_polling\_time\_seconds) | The amount of time in seconds to wait between each athena poll | `number` | `15` | no |
| [aws\_account\_id](#input\_aws\_account\_id) | The AWS Account ID (numeric) | `string` | n/a | yes |
| [component](#input\_component) | The variable encapsulating the name of this component | `string` | `"dl"` | no |
| [core\_notify\_url](#input\_core\_notify\_url) | The URL used to send requests to Notify | `string` | `"https://sandbox.api.service.nhs.uk"` | no |
@@ -59,6 +61,7 @@ No requirements.
| [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 |
+| [report\_generator](#module\_report\_generator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-lambda.zip | n/a |
| [report\_scheduler](#module\_report\_scheduler) | 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.30/terraform-s3bucket.zip | n/a |
| [s3bucket\_file\_quarantine](#module\_s3bucket\_file\_quarantine) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-s3bucket.zip | n/a |
@@ -77,6 +80,7 @@ No requirements.
| [sqs\_pdm\_uploader](#module\_sqs\_pdm\_uploader) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| [sqs\_print\_analyser](#module\_sqs\_print\_analyser) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| [sqs\_print\_status\_handler](#module\_sqs\_print\_status\_handler) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
+| [sqs\_report\_generator](#module\_sqs\_report\_generator) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| [sqs\_scanner](#module\_sqs\_scanner) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| [sqs\_ttl](#module\_sqs\_ttl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
| [sqs\_ttl\_handle\_expiry\_errors](#module\_sqs\_ttl\_handle\_expiry\_errors) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip | n/a |
diff --git a/infrastructure/terraform/components/dl/cloudwatch_event_rule_generate_report.tf b/infrastructure/terraform/components/dl/cloudwatch_event_rule_generate_report.tf
new file mode 100644
index 00000000..3d14b8d2
--- /dev/null
+++ b/infrastructure/terraform/components/dl/cloudwatch_event_rule_generate_report.tf
@@ -0,0 +1,18 @@
+resource "aws_cloudwatch_event_rule" "generate_report" {
+ name = "${local.csi}-generate-report"
+ description = "Generate Report event rule"
+ event_bus_name = aws_cloudwatch_event_bus.main.name
+ event_pattern = jsonencode({
+ "detail" : {
+ "type" : [
+ "uk.nhs.notify.digital.letters.reporting.generate.report.v1"
+ ],
+ }
+ })
+}
+
+resource "aws_cloudwatch_event_target" "generate_report_report_generator" {
+ rule = aws_cloudwatch_event_rule.generate_report.name
+ arn = module.sqs_report_generator.sqs_queue_arn
+ event_bus_name = aws_cloudwatch_event_bus.main.name
+}
diff --git a/infrastructure/terraform/components/dl/glue_catalog_table_event_record.tf b/infrastructure/terraform/components/dl/glue_catalog_table_event_record.tf
index f03c9586..b86d29c0 100644
--- a/infrastructure/terraform/components/dl/glue_catalog_table_event_record.tf
+++ b/infrastructure/terraform/components/dl/glue_catalog_table_event_record.tf
@@ -36,6 +36,10 @@ resource "aws_glue_catalog_table" "event_record" {
name = "type"
type = "string"
}
+ columns {
+ name = "letterstatus"
+ type = "string"
+ }
}
partition_keys {
diff --git a/infrastructure/terraform/components/dl/lambda_event_source_mapping_report_generator.tf b/infrastructure/terraform/components/dl/lambda_event_source_mapping_report_generator.tf
new file mode 100644
index 00000000..432181a6
--- /dev/null
+++ b/infrastructure/terraform/components/dl/lambda_event_source_mapping_report_generator.tf
@@ -0,0 +1,10 @@
+resource "aws_lambda_event_source_mapping" "report_generator" {
+ event_source_arn = module.sqs_report_generator.sqs_queue_arn
+ function_name = module.report_generator.function_name
+ batch_size = var.queue_batch_size
+ maximum_batching_window_in_seconds = var.queue_batch_window_seconds
+
+ function_response_types = [
+ "ReportBatchItemFailures"
+ ]
+}
diff --git a/infrastructure/terraform/components/dl/locals.tf b/infrastructure/terraform/components/dl/locals.tf
index 5838de64..301f11f0 100644
--- a/infrastructure/terraform/components/dl/locals.tf
+++ b/infrastructure/terraform/components/dl/locals.tf
@@ -3,6 +3,7 @@ locals {
apim_api_key_ssm_parameter_name = "/${var.component}/${var.environment}/apim/api_key"
apim_keystore_s3_bucket = "nhs-${var.aws_account_id}-${var.region}-${var.environment}-${var.component}-static-assets"
apim_private_key_ssm_parameter_name = "/${var.component}/${var.environment}/apim/private_key"
+ athena_reporting_database = "${local.csi}-reporting"
aws_lambda_functions_dir_path = "../../../../lambdas"
deploy_pdm_mock = var.enable_pdm_mock
firehose_output_path_prefix = "kinesis-firehose-output"
diff --git a/infrastructure/terraform/components/dl/module_lambda_report_generator.tf b/infrastructure/terraform/components/dl/module_lambda_report_generator.tf
new file mode 100644
index 00000000..fdc3f58a
--- /dev/null
+++ b/infrastructure/terraform/components/dl/module_lambda_report_generator.tf
@@ -0,0 +1,156 @@
+module "report_generator" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-lambda.zip"
+
+ function_name = "report-generator"
+ description = "A function to generator reports from an event"
+
+ 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
+
+ iam_policy_document = {
+ body = data.aws_iam_policy_document.report_generator_lambda.json
+ }
+
+ 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-generator/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
+
+ lambda_env_vars = {
+ "ATHENA_WORKGROUP" = aws_athena_workgroup.reporting.name
+ "ATHENA_DATABASE" = local.athena_reporting_database
+ "EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn
+ "EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url
+ "MAX_POLL_LIMIT" = var.athena_query_max_polling_attemps
+ "REPORTING_BUCKET" = module.s3bucket_reporting.bucket
+ "REPORT_NAME" = "completed_communications"
+ "WAIT_FOR_IN_SECONDS" = var.athena_query_polling_time_seconds
+ }
+}
+
+data "aws_iam_policy_document" "report_generator_lambda" {
+ statement {
+ sid = "AllowS3Get"
+ effect = "Allow"
+
+ actions = [
+ "s3:PutObject",
+ "s3:GetObject",
+ "s3:GetBucketLocation",
+ "s3:ListBucket"
+ ]
+
+ resources = [
+ "${module.s3bucket_reporting.arn}/*",
+ "${module.s3bucket_reporting.arn}"
+ ]
+ }
+
+ statement {
+ sid = "KMSPermissions"
+ effect = "Allow"
+
+ actions = [
+ "kms:Decrypt",
+ "kms:GenerateDataKey",
+ ]
+
+ resources = [
+ module.kms.key_arn,
+ ]
+ }
+
+ statement {
+ sid = "AllowAthenaAccess"
+ effect = "Allow"
+
+ actions = [
+ "athena:StartQueryExecution",
+ "athena:GetQueryResults",
+ "athena:GetQueryExecution"
+ ]
+
+ resources = [
+ "arn:aws:athena:${var.region}:${var.aws_account_id}:workgroup/${aws_athena_workgroup.reporting.name}"
+ ]
+ }
+
+ statement {
+ sid = "AllowGlueAccess"
+ effect = "Allow"
+
+ actions = [
+ "glue:GetTable",
+ "glue:GetDatabase",
+ "glue:GetPartition",
+ "glue:GetPartitions",
+ ]
+
+ resources = [
+ "arn:aws:glue:${var.region}:${var.aws_account_id}:catalog",
+ "arn:aws:glue:${var.region}:${var.aws_account_id}:database/${local.athena_reporting_database}",
+ "arn:aws:glue:${var.region}:${var.aws_account_id}:table/${local.athena_reporting_database}/*"
+ ]
+ }
+
+ statement {
+ sid = "SQSPermissionsReportGeneratorQueue"
+ effect = "Allow"
+
+ actions = [
+ "sqs:ReceiveMessage",
+ "sqs:DeleteMessage",
+ "sqs:GetQueueAttributes",
+ "sqs:GetQueueUrl",
+ ]
+
+ resources = [
+ module.sqs_report_generator.sqs_queue_arn,
+ ]
+ }
+
+ statement {
+ sid = "PutEvents"
+ effect = "Allow"
+
+ actions = [
+ "events:PutEvents",
+ ]
+
+ resources = [
+ aws_cloudwatch_event_bus.main.arn,
+ ]
+ }
+
+ statement {
+ sid = "SQSPermissionsEventPublisherDLQ"
+ effect = "Allow"
+
+ actions = [
+ "sqs:SendMessage",
+ "sqs:SendMessageBatch",
+ ]
+
+ resources = [
+ module.sqs_event_publisher_errors.sqs_queue_arn,
+ ]
+ }
+}
diff --git a/infrastructure/terraform/components/dl/module_sqs_report_generator.tf b/infrastructure/terraform/components/dl/module_sqs_report_generator.tf
new file mode 100644
index 00000000..a494ee60
--- /dev/null
+++ b/infrastructure/terraform/components/dl/module_sqs_report_generator.tf
@@ -0,0 +1,44 @@
+module "sqs_report_generator" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.30/terraform-sqs.zip"
+
+ aws_account_id = var.aws_account_id
+ component = local.component
+ environment = var.environment
+ project = var.project
+ region = var.region
+ name = "report-generator"
+
+ sqs_kms_key_arn = module.kms.key_arn
+
+ visibility_timeout_seconds = 60
+
+ create_dlq = true
+
+ sqs_policy_overload = data.aws_iam_policy_document.sqs_report_generator.json
+}
+
+data "aws_iam_policy_document" "sqs_report_generator" {
+ statement {
+ sid = "AllowEventBridgeToSendMessage"
+ effect = "Allow"
+
+ principals {
+ type = "Service"
+ identifiers = ["events.amazonaws.com"]
+ }
+
+ actions = [
+ "sqs:SendMessage"
+ ]
+
+ resources = [
+ "arn:aws:sqs:${var.region}:${var.aws_account_id}:${local.csi}-report-generator-queue"
+ ]
+
+ condition {
+ test = "ArnEquals"
+ variable = "aws:SourceArn"
+ values = [aws_cloudwatch_event_rule.generate_report.arn]
+ }
+ }
+}
diff --git a/infrastructure/terraform/components/dl/variables.tf b/infrastructure/terraform/components/dl/variables.tf
index 95a5d0fc..c5173e02 100644
--- a/infrastructure/terraform/components/dl/variables.tf
+++ b/infrastructure/terraform/components/dl/variables.tf
@@ -198,3 +198,15 @@ variable "default_cloudwatch_event_bus_name" {
description = "The name of the default cloudwatch event bus. This is needed as GuardDuty Scan Result events are sent to the default bus"
default = "default"
}
+
+variable "athena_query_max_polling_attemps" {
+ type = number
+ description = "The number of times athena will be polled to check if a query is completed"
+ default = 50
+}
+
+variable "athena_query_polling_time_seconds" {
+ type = number
+ description = "The amount of time in seconds to wait between each athena poll"
+ default = 15
+}
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
index 10942086..f1babbc4 100644
--- a/lambdas/report-event-transformer/src/__tests__/apis/firehose-handler.test.ts
+++ b/lambdas/report-event-transformer/src/__tests__/apis/firehose-handler.test.ts
@@ -49,6 +49,7 @@ describe('Firehose Handler', () => {
senderId: digitalLettersEvent.data.senderId,
pageCount: digitalLettersEvent.data.pageCount,
supplierId: digitalLettersEvent.data.supplierId,
+ letterStatus: digitalLettersEvent.data.status,
time: digitalLettersEvent.time,
type: digitalLettersEvent.type,
});
diff --git a/lambdas/report-event-transformer/src/__tests__/test-data.ts b/lambdas/report-event-transformer/src/__tests__/test-data.ts
index e8043885..91cacb2a 100644
--- a/lambdas/report-event-transformer/src/__tests__/test-data.ts
+++ b/lambdas/report-event-transformer/src/__tests__/test-data.ts
@@ -21,6 +21,7 @@ const baseEvent = {
resourceId: 'a2bcbb42-ab7e-42b6-88d6-74f8d3ca4a09',
messageReference: 'ref1',
senderId: 'sender1',
+ status: 'DISPATCHED',
},
};
diff --git a/lambdas/report-event-transformer/src/apis/firehose-handler.ts b/lambdas/report-event-transformer/src/apis/firehose-handler.ts
index 171003a2..5921bf9a 100644
--- a/lambdas/report-event-transformer/src/apis/firehose-handler.ts
+++ b/lambdas/report-event-transformer/src/apis/firehose-handler.ts
@@ -56,7 +56,7 @@ function validateRecord(
}
function generateReportEvent(validatedRecord: ValidatedRecord): ReportEvent {
- const { messageReference, pageCount, senderId, supplierId } =
+ const { messageReference, pageCount, senderId, status, supplierId } =
validatedRecord.event.data;
const { time, type } = validatedRecord.event;
const eventTime = new Date(time);
@@ -66,6 +66,7 @@ function generateReportEvent(validatedRecord: ValidatedRecord): ReportEvent {
senderId,
pageCount,
supplierId,
+ letterStatus: status,
time,
type,
};
diff --git a/lambdas/report-event-transformer/src/types/events.ts b/lambdas/report-event-transformer/src/types/events.ts
index c018f0f2..5a75e68d 100644
--- a/lambdas/report-event-transformer/src/types/events.ts
+++ b/lambdas/report-event-transformer/src/types/events.ts
@@ -6,6 +6,7 @@ export const $DigitalLettersEvent = z.object({
senderId: z.string(),
pageCount: z.number().optional(),
supplierId: z.string().optional(),
+ status: z.string().optional(),
}),
time: z.string(),
type: z.string(),
@@ -18,6 +19,7 @@ export type FlatDigitalLettersEvent = {
senderId: string;
pageCount?: number;
supplierId?: string;
+ letterStatus?: string;
time: string;
type: string;
};
diff --git a/lambdas/report-generator/jest.config.ts b/lambdas/report-generator/jest.config.ts
new file mode 100644
index 00000000..43ec9858
--- /dev/null
+++ b/lambdas/report-generator/jest.config.ts
@@ -0,0 +1,14 @@
+import { baseJestConfig } from '../../jest.config.base';
+
+const config = baseJestConfig;
+
+config.coverageThreshold = {
+ global: {
+ branches: 84,
+ functions: 100,
+ lines: 95,
+ statements: -10,
+ },
+};
+
+export default config;
diff --git a/lambdas/report-generator/package.json b/lambdas/report-generator/package.json
new file mode 100644
index 00000000..34b50efc
--- /dev/null
+++ b/lambdas/report-generator/package.json
@@ -0,0 +1,24 @@
+{
+ "dependencies": {
+ "@aws-sdk/client-athena": "^3.984.0",
+ "digital-letters-events": "^0.0.1",
+ "utils": "^0.0.1"
+ },
+ "devDependencies": {
+ "@tsconfig/node22": "^22.0.2",
+ "@types/aws-lambda": "^8.10.155",
+ "@types/jest": "^29.5.14",
+ "jest": "^29.7.0",
+ "typescript": "^5.9.3"
+ },
+ "name": "nhs-notify-digital-letters-report-generator",
+ "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 && cp -r src/queries dist/",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:unit": "jest",
+ "typecheck": "tsc --noEmit"
+ },
+ "version": "0.0.1"
+}
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
new file mode 100644
index 00000000..dca70fe2
--- /dev/null
+++ b/lambdas/report-generator/src/__tests__/apis/sqs-trigger-lambda.test.ts
@@ -0,0 +1,274 @@
+import { randomUUID } from 'node:crypto';
+import { GenerateReport } from 'digital-letters-events';
+import { EventPublisher, Logger } from 'utils';
+import type { SQSBatchResponse, SQSEvent, SQSRecord } from 'aws-lambda';
+import type {
+ ReportGenerator,
+ ReportGeneratorResult,
+} from 'app/report-generator';
+import { createHandler } from 'apis/sqs-trigger-lambda';
+
+jest.mock('node:crypto');
+
+const mockUuid = '123e4567-e89b-12d3-a456-426614174000';
+
+const createMockSQSRecord = (
+ messageId: string,
+ event: Partial,
+): SQSRecord => ({
+ messageId,
+ receiptHandle: 'receipt-handle',
+ body: JSON.stringify({
+ detail: {
+ id: event.id || mockUuid,
+ source:
+ event.source ||
+ '/nhs/england/notify/development/primary/data-plane/digitalletters/reporting',
+ specversion: event.specversion || '1.0',
+ type:
+ event.type ||
+ 'uk.nhs.notify.digital.letters.reporting.generate.report.v1',
+ dataschema:
+ event.dataschema ||
+ 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-reporting-generate-report-data.schema.json',
+ time: event.time || new Date().toISOString(),
+ recordedtime: event.recordedtime || new Date().toISOString(),
+ subject: event.subject || 'customer/5661de82-7453-44a1-9922-e0c98e5411c1',
+ traceparent:
+ event.traceparent ||
+ '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
+ severitynumber: event.severitynumber || 2,
+ data: event.data || {
+ senderId: 'fce3ddee-2aca-4b2e-90a8-ce4da3787792',
+ reportDate: '2025-01-01',
+ },
+ },
+ }),
+ attributes: {
+ ApproximateReceiveCount: '1',
+ SentTimestamp: '1234567890',
+ SenderId: 'sender-id',
+ ApproximateFirstReceiveTimestamp: '1234567890',
+ },
+ messageAttributes: {},
+ md5OfBody: 'md5',
+ eventSource: 'aws:sqs',
+ eventSourceARN: 'arn:aws:sqs:region:account:queue',
+ awsRegion: 'us-east-1',
+});
+
+const createMockSQSEvent = (records: SQSRecord[]): SQSEvent => ({
+ Records: records,
+});
+
+describe('sqs-trigger-lambda', () => {
+ let mockReportGenerator: jest.Mocked;
+ let mockEventPublisher: jest.Mocked;
+ let mockLogger: jest.Mocked;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (randomUUID as jest.Mock).mockReturnValue(mockUuid);
+
+ mockReportGenerator = {
+ generate: jest.fn(),
+ } as unknown as jest.Mocked;
+
+ mockEventPublisher = {
+ sendEvents: jest.fn().mockResolvedValue([]),
+ } as unknown as jest.Mocked;
+
+ mockLogger = {
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ } as unknown as jest.Mocked;
+ });
+
+ describe('createHandler', () => {
+ it('should process valid SQS records successfully', async () => {
+ const reportUri = 's3://bucket/report.csv';
+ const mockResult: ReportGeneratorResult = {
+ outcome: 'generated',
+ reportUri,
+ };
+ mockReportGenerator.generate.mockResolvedValue(mockResult);
+
+ const record = createMockSQSRecord('msg-1', {});
+ const sqsEvent = createMockSQSEvent([record]);
+
+ const handler = createHandler({
+ reportGenerator: mockReportGenerator,
+ eventPublisher: mockEventPublisher,
+ logger: mockLogger,
+ });
+
+ const response: SQSBatchResponse = await handler(sqsEvent);
+
+ expect(response.batchItemFailures).toEqual([]);
+ expect(mockReportGenerator.generate).toHaveBeenCalledTimes(1);
+ expect(mockEventPublisher.sendEvents).toHaveBeenCalledTimes(1);
+ expect(mockLogger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: 'Processed SQS Event.',
+ retrieved: 1,
+ generated: 1,
+ failed: 0,
+ }),
+ );
+ });
+
+ it('should handle invalid JSON in SQS record body', async () => {
+ const sqsEvent: SQSEvent = {
+ Records: [
+ {
+ ...createMockSQSRecord('msg-1', {}),
+ body: 'invalid-json',
+ },
+ ],
+ };
+
+ const handler = createHandler({
+ reportGenerator: mockReportGenerator,
+ eventPublisher: mockEventPublisher,
+ logger: mockLogger,
+ });
+
+ const response = await handler(sqsEvent);
+
+ expect(response.batchItemFailures).toEqual([{ itemIdentifier: 'msg-1' }]);
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: 'Error parsing SQS record',
+ }),
+ );
+ expect(mockReportGenerator.generate).not.toHaveBeenCalled();
+ });
+
+ it('should handle validation failure for event schema', async () => {
+ const record = createMockSQSRecord('msg-1', {
+ type: 'invalid-type' as any,
+ data: {} as any,
+ });
+ const sqsEvent = createMockSQSEvent([record]);
+
+ const handler = createHandler({
+ reportGenerator: mockReportGenerator,
+ eventPublisher: mockEventPublisher,
+ logger: mockLogger,
+ });
+
+ const response = await handler(sqsEvent);
+
+ expect(response.batchItemFailures).toEqual([{ itemIdentifier: 'msg-1' }]);
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ expect.objectContaining({
+ description: 'Error parsing queue entry',
+ }),
+ );
+ expect(mockReportGenerator.generate).not.toHaveBeenCalled();
+ });
+
+ it('should add to batch failures when report generation fails', async () => {
+ const mockResult: ReportGeneratorResult = { outcome: 'failed' };
+ mockReportGenerator.generate.mockResolvedValue(mockResult);
+
+ const record = createMockSQSRecord('msg-1', {});
+ const sqsEvent = createMockSQSEvent([record]);
+
+ const handler = createHandler({
+ reportGenerator: mockReportGenerator,
+ eventPublisher: mockEventPublisher,
+ logger: mockLogger,
+ });
+
+ const response = await handler(sqsEvent);
+
+ expect(response.batchItemFailures).toEqual([{ itemIdentifier: 'msg-1' }]);
+ expect(mockLogger.info).toHaveBeenCalledWith(
+ expect.objectContaining({
+ failed: 1,
+ generated: 0,
+ }),
+ );
+ });
+
+ it('should handle exceptions during report generation', async () => {
+ const error = new Error('Generation error');
+ mockReportGenerator.generate.mockRejectedValue(error);
+
+ const record = createMockSQSRecord('msg-1', {});
+ const sqsEvent = createMockSQSEvent([record]);
+
+ const handler = createHandler({
+ reportGenerator: mockReportGenerator,
+ eventPublisher: mockEventPublisher,
+ logger: mockLogger,
+ });
+
+ const response = await handler(sqsEvent);
+
+ expect(response.batchItemFailures).toEqual([{ itemIdentifier: 'msg-1' }]);
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ expect.objectContaining({
+ err: error,
+ description: 'Error during SQS trigger handler',
+ }),
+ );
+ });
+
+ it('should not publish events when there are no successful items', async () => {
+ mockReportGenerator.generate.mockResolvedValue({ outcome: 'failed' });
+
+ const record = createMockSQSRecord('msg-1', {
+ data: { senderId: 'sender-123' } as any,
+ });
+ const sqsEvent = createMockSQSEvent([record]);
+
+ const handler = createHandler({
+ reportGenerator: mockReportGenerator,
+ eventPublisher: mockEventPublisher,
+ logger: mockLogger,
+ });
+
+ await handler(sqsEvent);
+
+ expect(mockEventPublisher.sendEvents).not.toHaveBeenCalled();
+ });
+
+ it('should generate correct ReportGenerated events', async () => {
+ const reportUri = 's3://bucket/report.csv';
+ mockReportGenerator.generate.mockResolvedValue({
+ outcome: 'generated',
+ reportUri,
+ });
+
+ const record = createMockSQSRecord('msg-1', {});
+ const sqsEvent = createMockSQSEvent([record]);
+
+ const handler = createHandler({
+ reportGenerator: mockReportGenerator,
+ eventPublisher: mockEventPublisher,
+ logger: mockLogger,
+ });
+
+ await handler(sqsEvent);
+
+ expect(mockEventPublisher.sendEvents).toHaveBeenCalledWith(
+ [
+ expect.objectContaining({
+ id: mockUuid,
+ type: 'uk.nhs.notify.digital.letters.reporting.report.generated.v1',
+ dataschema:
+ 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-reporting-report-generated-data.schema.json',
+ data: {
+ senderId: 'fce3ddee-2aca-4b2e-90a8-ce4da3787792',
+ reportUri,
+ },
+ }),
+ ],
+ expect.any(Function),
+ );
+ });
+ });
+});
diff --git a/lambdas/report-generator/src/__tests__/app/report-generator.test.ts b/lambdas/report-generator/src/__tests__/app/report-generator.test.ts
new file mode 100644
index 00000000..95b497c2
--- /dev/null
+++ b/lambdas/report-generator/src/__tests__/app/report-generator.test.ts
@@ -0,0 +1,168 @@
+import { IReportService, Logger } from 'utils';
+import { GenerateReport } from 'digital-letters-events';
+import fs from 'node:fs';
+import { ReportGenerator } from 'app/report-generator';
+
+jest.mock('node:fs');
+
+describe('ReportGenerator', () => {
+ let mockLogger: jest.Mocked;
+ let mockReportService: jest.Mocked;
+ let reportGenerator: ReportGenerator;
+ const reportName = 'completed_communications';
+
+ beforeEach(() => {
+ mockLogger = {
+ info: jest.fn(),
+ error: jest.fn(),
+ warn: jest.fn(),
+ debug: jest.fn(),
+ } as unknown as jest.Mocked;
+
+ mockReportService = {
+ generateReport: jest.fn(),
+ } as jest.Mocked;
+
+ reportGenerator = new ReportGenerator(
+ mockLogger,
+ mockReportService,
+ reportName,
+ );
+
+ jest.clearAllMocks();
+ });
+
+ describe('generate', () => {
+ const mockEvent: GenerateReport = {
+ data: {
+ senderId: 'sender-123',
+ reportDate: '2025-01-15',
+ },
+ specversion: '1.0',
+ type: 'uk.nhs.notify.digital.letters.reporting.generate.report.v1',
+ source: 'test',
+ id: 'test-id',
+ time: '2025-01-15T10:00:00Z',
+ datacontenttype: 'application/json',
+ subject: 'customer/5661de82-7453-44a1-9922-e0c98e5411c1',
+ traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01',
+ recordedtime: '2025-12-15T10:00:00Z',
+ severitynumber: 2,
+ };
+
+ const mockQuery =
+ 'SELECT * FROM reports WHERE date = $1 AND sender_id = $2';
+
+ beforeEach(() => {
+ (fs.readFileSync as jest.Mock).mockReturnValue(mockQuery);
+ });
+
+ it('should successfully generate a report', async () => {
+ const expectedLocation =
+ 's3://bucket/reports/sender-123/completed_communications/completed_communications_sender-123_2025-01-15.csv';
+ mockReportService.generateReport.mockResolvedValue(expectedLocation);
+
+ const result = await reportGenerator.generate(mockEvent);
+
+ expect(fs.readFileSync).toHaveBeenCalledWith(
+ '/var/task/queries/report.sql',
+ 'utf8',
+ );
+ expect(mockLogger.info).toHaveBeenCalledWith(
+ 'Generating report for sender sender-123 and date 2025-01-15',
+ );
+ expect(mockReportService.generateReport).toHaveBeenCalledWith(
+ mockQuery,
+ ["'2025-01-15'", "'sender-123'"],
+ 'transactional-reports/sender-123/completed_communications/completed_communications_2025-01-15.csv',
+ );
+ expect(result).toEqual({
+ outcome: 'generated',
+ reportUri: expectedLocation,
+ });
+ });
+
+ it('should construct correct report file path with report name', async () => {
+ const expectedLocation =
+ 's3://bucket/transactional-reports/sender-123/completed_communications/completed_communications_2025-01-15.csv';
+ mockReportService.generateReport.mockResolvedValue(expectedLocation);
+
+ const customEvent: GenerateReport = {
+ ...mockEvent,
+ data: {
+ senderId: 'sender-456',
+ reportDate: '2025-02-20',
+ },
+ };
+
+ await reportGenerator.generate(customEvent);
+
+ expect(mockReportService.generateReport).toHaveBeenCalledWith(
+ expect.any(String),
+ ["'2025-02-20'", "'sender-456'"],
+ 'transactional-reports/sender-456/completed_communications/completed_communications_2025-02-20.csv',
+ );
+ });
+
+ it('should pass query parameters in correct order', async () => {
+ mockReportService.generateReport.mockResolvedValue(
+ 's3://bucket/report.csv',
+ );
+
+ await reportGenerator.generate(mockEvent);
+
+ expect(mockReportService.generateReport).toHaveBeenCalledWith(
+ mockQuery,
+ ["'2025-01-15'", "'sender-123'"],
+ expect.any(String),
+ );
+ });
+
+ it('should return failed outcome when report service throws error', async () => {
+ const error = new Error('Database connection failed');
+ mockReportService.generateReport.mockRejectedValue(error);
+
+ const result = await reportGenerator.generate(mockEvent);
+
+ expect(mockLogger.error).toHaveBeenCalledWith({
+ err: error,
+ description: 'Error generating report',
+ senderId: 'sender-123',
+ reportDate: '2025-01-15',
+ });
+ expect(result).toEqual({
+ outcome: 'failed',
+ });
+ });
+
+ it('should return failed outcome when file read throws error', async () => {
+ const error = new Error('File not found');
+ (fs.readFileSync as jest.Mock).mockImplementation(() => {
+ throw error;
+ });
+
+ const result = await reportGenerator.generate(mockEvent);
+
+ expect(mockLogger.error).toHaveBeenCalledWith({
+ err: error,
+ description: 'Error generating report',
+ senderId: 'sender-123',
+ reportDate: '2025-01-15',
+ });
+ expect(result).toEqual({
+ outcome: 'failed',
+ });
+ });
+
+ it('should not return reportUri when report generation fails', async () => {
+ mockReportService.generateReport.mockRejectedValue(
+ new Error('S3 upload failed'),
+ );
+
+ const result = await reportGenerator.generate(mockEvent);
+
+ expect(result.reportUri).toBeUndefined();
+ expect(result.outcome).toBe('failed');
+ });
+ });
+});
diff --git a/lambdas/report-generator/src/__tests__/container.test.ts b/lambdas/report-generator/src/__tests__/container.test.ts
new file mode 100644
index 00000000..cbea0df4
--- /dev/null
+++ b/lambdas/report-generator/src/__tests__/container.test.ts
@@ -0,0 +1,32 @@
+import { createContainer } from 'container';
+
+jest.mock('infra/config', () => ({
+ loadConfig: jest.fn(() => ({
+ athenaDatabase: 'test-database',
+ athenaWorkgroup: 'test-workgroup',
+ eventPublisherDlqUrl: 'test-url',
+ eventPublisherEventBusArn: 'test-arn',
+ maxPollLimit: 10,
+ reportName: 'test-report',
+ reportingBucket: 'test-bucket',
+ waitForInSeconds: 5,
+ })),
+}));
+
+jest.mock('utils', () => ({
+ AthenaRepository: jest.fn(() => ({})),
+ ReportService: jest.fn(() => ({})),
+ createStorageRepository: jest.fn(() => ({})),
+ s3Client: {},
+ eventBridgeClient: {},
+ EventPublisher: jest.fn(() => ({})),
+ logger: {},
+ sqsClient: {},
+}));
+
+describe('container', () => {
+ it('should create container', () => {
+ const container = createContainer();
+ expect(container).toBeDefined();
+ });
+});
diff --git a/lambdas/report-generator/src/__tests__/index.test.ts b/lambdas/report-generator/src/__tests__/index.test.ts
new file mode 100644
index 00000000..6241b09f
--- /dev/null
+++ b/lambdas/report-generator/src/__tests__/index.test.ts
@@ -0,0 +1,11 @@
+import { handler } from 'index';
+
+jest.mock('container', () => ({
+ createContainer: jest.fn(() => ({})),
+}));
+
+describe('index', () => {
+ it('should export handler', () => {
+ expect(handler).toBeDefined();
+ });
+});
diff --git a/lambdas/report-generator/src/__tests__/infra/config.test.ts b/lambdas/report-generator/src/__tests__/infra/config.test.ts
new file mode 100644
index 00000000..2902c80f
--- /dev/null
+++ b/lambdas/report-generator/src/__tests__/infra/config.test.ts
@@ -0,0 +1,15 @@
+import { loadConfig } from 'infra/config';
+
+jest.mock('utils', () => ({
+ defaultConfigReader: {
+ getValue: jest.fn(),
+ getInt: jest.fn(),
+ },
+}));
+
+describe('config', () => {
+ it('should load config', () => {
+ const config = loadConfig();
+ expect(config).toBeDefined();
+ });
+});
diff --git a/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts b/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts
new file mode 100644
index 00000000..b81f9fcf
--- /dev/null
+++ b/lambdas/report-generator/src/apis/sqs-trigger-lambda.ts
@@ -0,0 +1,211 @@
+import type {
+ SQSBatchItemFailure,
+ SQSBatchResponse,
+ SQSEvent,
+} from 'aws-lambda';
+import { randomUUID } from 'node:crypto';
+import type {
+ ReportGenerator,
+ 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 { EventPublisher, Logger } from 'utils';
+
+interface ProcessingResult {
+ result: ReportGeneratorResult;
+ item?: GenerateReport;
+}
+
+interface CreateHandlerDependencies {
+ reportGenerator: ReportGenerator;
+ eventPublisher: EventPublisher;
+ logger: Logger;
+}
+
+interface ValidatedRecord {
+ messageId: string;
+ event: GenerateReport;
+}
+
+function validateRecord(
+ { body, messageId }: { body: string; messageId: string },
+ logger: Logger,
+): ValidatedRecord | null {
+ try {
+ 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;
+ }
+
+ return { messageId, event: sqsEventDetail };
+ } catch (error) {
+ logger.error({
+ err: error,
+ description: 'Error parsing SQS record',
+ });
+ return null;
+ }
+}
+
+async function processRecord(
+ { event, messageId }: ValidatedRecord,
+ reportGenerator: ReportGenerator,
+ logger: Logger,
+ batchItemFailures: SQSBatchItemFailure[],
+): Promise {
+ try {
+ const result = await reportGenerator.generate(event);
+
+ if (result.outcome === 'failed') {
+ batchItemFailures.push({ itemIdentifier: messageId });
+ return { result: { outcome: 'failed' }, item: event };
+ }
+
+ return { result, item: event };
+ } catch (error) {
+ logger.error({
+ err: error,
+ description: 'Error during SQS trigger handler',
+ });
+ batchItemFailures.push({ itemIdentifier: messageId });
+ return { result: { outcome: 'failed' } };
+ }
+}
+
+interface CategorizedResults {
+ processed: Record;
+ successfulItems: { event: GenerateReport; reportUri: string }[];
+ failedItems: GenerateReport[];
+}
+
+function categorizeResults(
+ results: PromiseSettledResult[],
+ logger: Logger,
+): CategorizedResults {
+ const processed: Record = {
+ retrieved: results.length,
+ generated: 0,
+ failed: 0,
+ };
+
+ const successfulItems: {
+ event: GenerateReport;
+ reportUri: string;
+ }[] = [];
+ const failedItems: GenerateReport[] = [];
+
+ for (const result of results) {
+ if (result.status === 'fulfilled') {
+ const { item, result: itemResult } = result.value;
+ processed[itemResult.outcome] += 1;
+
+ if (item) {
+ if (itemResult.outcome === 'generated' && itemResult.reportUri) {
+ successfulItems.push({
+ event: item,
+ reportUri: itemResult.reportUri,
+ });
+ } else {
+ failedItems.push(item);
+ }
+ }
+ } else {
+ logger.error({ err: result.reason });
+ processed.failed += 1;
+ }
+ }
+
+ return { processed, successfulItems, failedItems };
+}
+
+async function publishSuccessfulEvents(
+ successfulItems: { event: GenerateReport; reportUri: string }[],
+ eventPublisher: EventPublisher,
+ logger: Logger,
+): Promise {
+ if (successfulItems.length === 0) return;
+
+ try {
+ const reportGeneratedEvents: ReportGenerated[] = successfulItems.map(
+ ({ event, reportUri }) => ({
+ ...event,
+ id: randomUUID(),
+ time: new Date().toISOString(),
+ recordedtime: new Date().toISOString(),
+ type: 'uk.nhs.notify.digital.letters.reporting.report.generated.v1',
+ dataschema:
+ 'https://notify.nhs.uk/cloudevents/schemas/digital-letters/2025-10-draft/data/digital-letters-reporting-report-generated-data.schema.json',
+ data: {
+ senderId: event.data.senderId,
+ reportUri,
+ },
+ }),
+ );
+
+ const submittedFailedEvents =
+ await eventPublisher.sendEvents(
+ reportGeneratedEvents,
+ reportGeneratedValidator,
+ );
+ if (submittedFailedEvents.length > 0) {
+ logger.warn({
+ description: 'Some successful events failed to publish',
+ failedCount: submittedFailedEvents.length,
+ totalAttempted: successfulItems.length,
+ });
+ }
+ } catch (error) {
+ logger.warn({
+ err: error,
+ description: 'Failed to send successful events to EventBridge',
+ eventCount: successfulItems.length,
+ });
+ }
+}
+
+export const createHandler = ({
+ eventPublisher,
+ logger,
+ reportGenerator,
+}: CreateHandlerDependencies) =>
+ async function handler(sqsEvent: SQSEvent): Promise {
+ const batchItemFailures: SQSBatchItemFailure[] = [];
+
+ const validatedRecords: ValidatedRecord[] = [];
+ for (const record of sqsEvent.Records) {
+ const validated = validateRecord(record, logger);
+ if (validated) {
+ validatedRecords.push(validated);
+ } else {
+ batchItemFailures.push({ itemIdentifier: record.messageId });
+ }
+ }
+
+ const promises = validatedRecords.map((record) =>
+ processRecord(record, reportGenerator, logger, batchItemFailures),
+ );
+
+ const results = await Promise.allSettled(promises);
+ const { processed, successfulItems } = categorizeResults(results, logger);
+
+ await publishSuccessfulEvents(successfulItems, eventPublisher, logger);
+
+ logger.info({
+ description: 'Processed SQS Event.',
+ ...processed,
+ });
+
+ return { batchItemFailures };
+ };
+
+export default createHandler;
diff --git a/lambdas/report-generator/src/app/report-generator.ts b/lambdas/report-generator/src/app/report-generator.ts
new file mode 100644
index 00000000..30837d58
--- /dev/null
+++ b/lambdas/report-generator/src/app/report-generator.ts
@@ -0,0 +1,47 @@
+import { IReportService, Logger } from 'utils';
+import { GenerateReport } from 'digital-letters-events';
+import fs from 'node:fs';
+
+export type ReportGeneratorOutcome = 'generated' | 'failed';
+
+export type ReportGeneratorResult = {
+ outcome: ReportGeneratorOutcome;
+ reportUri?: string;
+};
+
+export class ReportGenerator {
+ constructor(
+ private readonly logger: Logger,
+ private readonly reportService: IReportService,
+ private readonly reportName: string,
+ ) {}
+
+ async generate(event: GenerateReport): Promise {
+ try {
+ const query = fs.readFileSync('/var/task/queries/report.sql', 'utf8');
+ const { senderId } = event.data;
+ const { reportDate } = event.data;
+ const reportFilePath = `transactional-reports/${senderId}/${this.reportName}/${this.reportName}_${reportDate}.csv`;
+
+ this.logger.info(
+ `Generating report for sender ${senderId} and date ${reportDate}`,
+ );
+
+ const location = await this.reportService.generateReport(
+ query,
+ [`'${reportDate}'`, `'${senderId}'`],
+ reportFilePath,
+ );
+
+ return { outcome: 'generated', reportUri: location };
+ } catch (error) {
+ this.logger.error({
+ err: error,
+ description: 'Error generating report',
+ senderId: event.data.senderId,
+ reportDate: event.data.reportDate,
+ });
+ return { outcome: 'failed' };
+ }
+ }
+}
diff --git a/lambdas/report-generator/src/container.ts b/lambdas/report-generator/src/container.ts
new file mode 100644
index 00000000..947b71ef
--- /dev/null
+++ b/lambdas/report-generator/src/container.ts
@@ -0,0 +1,71 @@
+import {
+ AthenaRepository,
+ EventPublisher,
+ ReportService,
+ createStorageRepository,
+ eventBridgeClient,
+ logger,
+ s3Client,
+ sqsClient,
+} from 'utils';
+import { loadConfig } from 'infra/config';
+import { ReportGenerator } from 'app/report-generator';
+import { AthenaClient } from '@aws-sdk/client-athena';
+
+export const createContainer = () => {
+ const {
+ athenaDatabase,
+ athenaWorkgroup,
+ eventPublisherDlqUrl,
+ eventPublisherEventBusArn,
+ maxPollLimit,
+ reportName,
+ reportingBucket,
+ waitForInSeconds,
+ } = loadConfig();
+
+ const athenaClient = new AthenaClient({
+ region: 'eu-west-2',
+ });
+
+ const dataRepository = new AthenaRepository(athenaClient, {
+ athenaWorkgroup,
+ athenaDatabase,
+ });
+
+ const storageRepository = createStorageRepository({
+ s3Client,
+ reportingBucketName: reportingBucket,
+ logger,
+ });
+
+ const reportService = new ReportService(
+ dataRepository,
+ storageRepository,
+ maxPollLimit,
+ waitForInSeconds,
+ logger,
+ );
+
+ const reportGenerator = new ReportGenerator(
+ logger,
+ reportService,
+ reportName,
+ );
+
+ const eventPublisher = new EventPublisher({
+ eventBusArn: eventPublisherEventBusArn,
+ dlqUrl: eventPublisherDlqUrl,
+ logger,
+ sqsClient,
+ eventBridgeClient,
+ });
+
+ return {
+ reportGenerator,
+ eventPublisher,
+ logger,
+ };
+};
+
+export default createContainer;
diff --git a/lambdas/report-generator/src/index.ts b/lambdas/report-generator/src/index.ts
new file mode 100644
index 00000000..65f60969
--- /dev/null
+++ b/lambdas/report-generator/src/index.ts
@@ -0,0 +1,6 @@
+import { createHandler } from 'apis/sqs-trigger-lambda';
+import { createContainer } from 'container';
+
+export const handler = createHandler(createContainer());
+
+export default handler;
diff --git a/lambdas/report-generator/src/infra/config.ts b/lambdas/report-generator/src/infra/config.ts
new file mode 100644
index 00000000..b93c0d4e
--- /dev/null
+++ b/lambdas/report-generator/src/infra/config.ts
@@ -0,0 +1,29 @@
+import { defaultConfigReader } from 'utils';
+
+export type ReportGeneratorConfig = {
+ athenaWorkgroup: string;
+ athenaDatabase: string;
+ eventPublisherEventBusArn: string;
+ eventPublisherDlqUrl: string;
+ maxPollLimit: number;
+ reportingBucket: string;
+ reportName: string;
+ waitForInSeconds: number;
+};
+
+export function loadConfig(): ReportGeneratorConfig {
+ return {
+ athenaWorkgroup: defaultConfigReader.getValue('ATHENA_WORKGROUP'),
+ athenaDatabase: defaultConfigReader.getValue('ATHENA_DATABASE'),
+ eventPublisherEventBusArn: defaultConfigReader.getValue(
+ 'EVENT_PUBLISHER_EVENT_BUS_ARN',
+ ),
+ eventPublisherDlqUrl: defaultConfigReader.getValue(
+ 'EVENT_PUBLISHER_DLQ_URL',
+ ),
+ maxPollLimit: defaultConfigReader.getInt('MAX_POLL_LIMIT'),
+ reportingBucket: defaultConfigReader.getValue('REPORTING_BUCKET'),
+ reportName: defaultConfigReader.getValue('REPORT_NAME'),
+ waitForInSeconds: defaultConfigReader.getInt('WAIT_FOR_IN_SECONDS'),
+ };
+}
diff --git a/lambdas/report-generator/src/queries/report.sql b/lambdas/report-generator/src/queries/report.sql
new file mode 100644
index 00000000..3bdf0fd6
--- /dev/null
+++ b/lambdas/report-generator/src/queries/report.sql
@@ -0,0 +1,55 @@
+WITH vars AS (
+ SELECT CAST(? AS DATE) AS dt,
+ ? AS senderid
+),
+"translated_events" AS (
+ SELECT e.messagereference,
+ e.time,
+ CASE
+ WHEN e.type LIKE '%.item.dequeued.%'
+ OR e.type LIKE '%.item.removed.%' THEN 'Digital'
+ WHEN e.type LIKE '%.print.letter.transitioned.%' THEN 'Print' ELSE NULL
+ END as communicationtype,
+ CASE
+ WHEN e.type LIKE '%.item.dequeued.%' THEN 'Unread'
+ WHEN e.type LIKE '%.item.removed.%' THEN 'Read'
+ WHEN e.letterstatus = 'REJECTED' THEN 'Rejected'
+ WHEN e.letterstatus = 'FAILED' THEN 'Failed'
+ WHEN e.letterstatus = 'DISPATCHED' THEN 'Dispatched'
+ WHEN e.letterstatus = 'REJECTED' THEN 'Rejected' ELSE NULL
+ END as status
+ FROM event_record e
+ CROSS JOIN vars v
+ WHERE e.senderid = v.senderid
+ AND e.__year = year(v.dt)
+ AND e.__month = month(v.dt)
+ AND e.__day = day(v.dt)
+),
+"ordered_events" AS (
+ SELECT ROW_NUMBER() OVER (
+ PARTITION BY te.messagereference, te.communicationtype
+ ORDER BY te.time DESC,
+ CASE
+ -- Digital Priority Order
+ WHEN te.status = 'Read' THEN 2
+ WHEN te.status = 'Unread' THEN 1
+ -- Print Priority Order
+ WHEN te.status = 'Returned' THEN 4
+ WHEN te.status = 'Failed' THEN 3
+ WHEN te.status = 'Dispatched' THEN 2
+ WHEN te.status = 'Rejected' THEN 1 ELSE 0
+ END DESC
+ ) AS "row_number",
+ te.messagereference,
+ te.time,
+ te.communicationtype,
+ te.status
+ FROM "translated_events" AS te
+ where te.status IS NOT NULL
+)
+SELECT oe.messagereference,
+ oe.time,
+ oe.communicationtype,
+ oe.status
+FROM "ordered_events" AS oe
+WHERE oe.row_number = 1
diff --git a/lambdas/report-generator/tsconfig.json b/lambdas/report-generator/tsconfig.json
new file mode 100644
index 00000000..f7bcaa1f
--- /dev/null
+++ b/lambdas/report-generator/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "baseUrl": "./src/",
+ "isolatedModules": true
+ },
+ "extends": "@tsconfig/node22/tsconfig.json",
+ "include": [
+ "src/**/*",
+ "jest.config.ts"
+ ]
+}
diff --git a/lambdas/ttl-poll-lambda/src/__tests__/infra/dynamo-repository.test.ts b/lambdas/ttl-poll-lambda/src/__tests__/infra/dynamo-repository.test.ts
index 5f4d579f..b978f78a 100644
--- a/lambdas/ttl-poll-lambda/src/__tests__/infra/dynamo-repository.test.ts
+++ b/lambdas/ttl-poll-lambda/src/__tests__/infra/dynamo-repository.test.ts
@@ -4,7 +4,7 @@ import {
QueryCommand,
} from '@aws-sdk/lib-dynamodb';
import { mockClient } from 'aws-sdk-client-mock';
-import { Logger, deleteDynamoBatch, dynamoDocumentClient } from 'utils';
+import { Logger, deleteDynamoBatch } from 'utils';
import { mock, mockFn } from 'jest-mock-extended';
import { DynamoRepository } from 'infra/dynamo-repository';
import 'aws-sdk-client-mock-jest';
@@ -20,7 +20,7 @@ const mockDynamoDeleteBatch = mockFn();
const dynamoRepository = new DynamoRepository(
mockTableName,
- dynamoDocumentClient,
+ mockDynamoClient as any,
logger,
mockDynamoDeleteBatch,
);
diff --git a/package-lock.json b/package-lock.json
index c36f2fb5..0abe3ad4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,6 +23,7 @@
"lambdas/report-scheduler",
"lambdas/report-event-transformer",
"lambdas/move-scanned-files-lambda",
+ "lambdas/report-generator",
"utils/utils",
"utils/sender-management",
"src/cloudevents",
@@ -3373,6 +3374,313 @@
"dev": true,
"license": "MIT"
},
+ "lambdas/report-generator": {
+ "name": "nhs-notify-digital-letters-report-generator",
+ "version": "0.0.1",
+ "dependencies": {
+ "@aws-sdk/client-athena": "^3.984.0",
+ "digital-letters-events": "^0.0.1",
+ "utils": "^0.0.1"
+ },
+ "devDependencies": {
+ "@tsconfig/node22": "^22.0.2",
+ "@types/aws-lambda": "^8.10.155",
+ "@types/jest": "^29.5.14",
+ "jest": "^29.7.0",
+ "typescript": "^5.9.3"
+ }
+ },
+ "lambdas/report-generator/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-generator/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-generator/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-generator/node_modules/@sinclair/typebox": {
+ "version": "0.27.10",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
+ "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "lambdas/report-generator/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-generator/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-generator/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",
+ "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-generator/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-generator/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-generator/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-generator/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-generator/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-generator/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-generator/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-generator/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/report-scheduler": {
"name": "nhs-notify-digital-letters-report-scheduler-lambda",
"version": "0.0.1",
@@ -4841,6 +5149,72 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@aws-sdk/client-athena": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-athena/-/client-athena-3.986.0.tgz",
+ "integrity": "sha512-mZWMvRzr6H9yQ5qKNSe2Fc1lfCNuhdhU+9eV0/qsNyT1t6r1yIfual/o0nBWTNVD9yt1k7J+tjxAY2ElyANAQw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-node": "^3.972.6",
+ "@aws-sdk/middleware-host-header": "^3.972.3",
+ "@aws-sdk/middleware-logger": "^3.972.3",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.3",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
+ "@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.986.0",
+ "@aws-sdk/util-user-agent-browser": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/core": "^3.22.1",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/hash-node": "^4.2.8",
+ "@smithy/invalid-dependency": "^4.2.8",
+ "@smithy/middleware-content-length": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/node-http-handler": "^4.4.9",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.11.2",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-athena/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.986.0.tgz",
+ "integrity": "sha512-Mqi79L38qi1gCG3adlVdbNrSxvcm1IPDLiJPA3OBypY5ewxUyWbaA3DD4goG+EwET6LSFgZJcRSIh6KBNpP5pA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/client-cloudwatch-logs": {
"version": "3.981.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.981.0.tgz",
@@ -4899,6 +5273,7 @@
"resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.981.0.tgz",
"integrity": "sha512-QFM/3LbHzjydyKbuVA+oYPc3QCNH8hjui6Te/AaDO2waw/jLUklzItQZPHRQk6vMKnd7bNDUzNX0UiYTqrU5QA==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
@@ -4947,195 +5322,155 @@
"node": ">=20.0.0"
}
},
- "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/client-dynamodb": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.980.0.tgz",
- "integrity": "sha512-1rGhAx4cHZy3pMB3R3r84qMT5WEvQ6ajr2UksnD48fjQxwaUcpI6NsPvU5j/5BI5LqGiUO6ThOrMwSMm95twQA==",
+ "node_modules/@aws-sdk/client-eventbridge": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-eventbridge/-/client-eventbridge-3.986.0.tgz",
+ "integrity": "sha512-/pn1HraGQvH3B7fr1H8BmDn0m7QPgGY+kwJHOWVGr+4+uME87TZWnTcCUvq9Zk+i6ciz9isgQcGg1JGEgJnTyg==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/credential-provider-node": "^3.972.4",
- "@aws-sdk/dynamodb-codec": "^3.972.5",
- "@aws-sdk/middleware-endpoint-discovery": "^3.972.3",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-node": "^3.972.6",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/signature-v4-multi-region": "3.986.0",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
+ "@aws-sdk/util-endpoints": "3.986.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
+ "@smithy/core": "^3.22.1",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
"@smithy/util-utf8": "^4.2.0",
- "@smithy/util-waiter": "^4.2.8",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
- "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
- "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
+ "node_modules/@aws-sdk/client-eventbridge/node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.986.0.tgz",
+ "integrity": "sha512-Upw+rw7wCH93E6QWxqpAqJLrUmJYVUAWrk4tCOBnkeuwzGERZvJFL5UQ6TAJFj9T18Ih+vNFaACh8J5aP4oTBw==",
"license": "Apache-2.0",
"dependencies": {
+ "@aws-sdk/middleware-sdk-s3": "^3.972.7",
"@aws-sdk/types": "^3.973.1",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/signature-v4": "^5.3.8",
"@smithy/types": "^4.12.0",
- "@smithy/url-parser": "^4.2.8",
- "@smithy/util-endpoints": "^3.2.8",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
- "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/dynamodb-codec": {
- "version": "3.972.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.5.tgz",
- "integrity": "sha512-gFR4w3dIkaZ82kFFjil7RFtukS2y2fXrDNDfgc94DhKjjOQMJEcHM5o1GGaQE4jd2mOQfHvbeQ0ktU8xGXhHjQ==",
+ "node_modules/@aws-sdk/client-eventbridge/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.986.0.tgz",
+ "integrity": "sha512-Mqi79L38qi1gCG3adlVdbNrSxvcm1IPDLiJPA3OBypY5ewxUyWbaA3DD4goG+EwET6LSFgZJcRSIh6KBNpP5pA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@smithy/core": "^3.22.0",
- "@smithy/smithy-client": "^4.11.1",
+ "@aws-sdk/types": "^3.973.1",
"@smithy/types": "^4.12.0",
- "@smithy/util-base64": "^4.3.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
- },
- "peerDependencies": {
- "@aws-sdk/client-dynamodb": "3.980.0"
}
},
- "node_modules/@aws-sdk/client-eventbridge": {
- "version": "3.981.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-eventbridge/-/client-eventbridge-3.981.0.tgz",
- "integrity": "sha512-ZXiLE8HA8CFxSZDTLt8PkrZd51bTrWKZH4Shj6F9LAQsDrx++tb4EFJ4NhoamsmrqEXZxQCYVriGmpnzhoBnhg==",
+ "node_modules/@aws-sdk/client-lambda": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.986.0.tgz",
+ "integrity": "sha512-R0VrqSH622b0MmIULLCNbupyU9qqEn+vofIeKng+ALPJY6U7pq7MG0p+bbxLCGztLl0u2vmO237SPZYcFm3hCQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/credential-provider-node": "^3.972.4",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-node": "^3.972.6",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
- "@aws-sdk/signature-v4-multi-region": "3.981.0",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.981.0",
+ "@aws-sdk/util-endpoints": "3.986.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
+ "@smithy/core": "^3.22.1",
+ "@smithy/eventstream-serde-browser": "^4.2.8",
+ "@smithy/eventstream-serde-config-resolver": "^4.3.8",
+ "@smithy/eventstream-serde-node": "^4.2.8",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
+ "@smithy/util-stream": "^4.5.11",
"@smithy/util-utf8": "^4.2.0",
+ "@smithy/util-waiter": "^4.2.8",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
- "node_modules/@aws-sdk/client-lambda": {
- "version": "3.981.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.981.0.tgz",
- "integrity": "sha512-zLQpFFdKaXyARxpCuGcfax5ZpAguPo/G2uVCBJjDAycT7FU+spTPttV5dI49GQIo5pvU8yn+EaCVBJl89mZy1g==",
+ "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.986.0.tgz",
+ "integrity": "sha512-Mqi79L38qi1gCG3adlVdbNrSxvcm1IPDLiJPA3OBypY5ewxUyWbaA3DD4goG+EwET6LSFgZJcRSIh6KBNpP5pA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-crypto/sha256-browser": "5.2.0",
- "@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/credential-provider-node": "^3.972.4",
- "@aws-sdk/middleware-host-header": "^3.972.3",
- "@aws-sdk/middleware-logger": "^3.972.3",
- "@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
- "@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.981.0",
- "@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
- "@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
- "@smithy/eventstream-serde-browser": "^4.2.8",
- "@smithy/eventstream-serde-config-resolver": "^4.3.8",
- "@smithy/eventstream-serde-node": "^4.2.8",
- "@smithy/fetch-http-handler": "^5.3.9",
- "@smithy/hash-node": "^4.2.8",
- "@smithy/invalid-dependency": "^4.2.8",
- "@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
- "@smithy/middleware-serde": "^4.2.9",
- "@smithy/middleware-stack": "^4.2.8",
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
- "@smithy/util-base64": "^4.3.0",
- "@smithy/util-body-length-browser": "^4.2.0",
- "@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
"@smithy/util-endpoints": "^3.2.8",
- "@smithy/util-middleware": "^4.2.8",
- "@smithy/util-retry": "^4.2.8",
- "@smithy/util-stream": "^4.5.10",
- "@smithy/util-utf8": "^4.2.0",
- "@smithy/util-waiter": "^4.2.8",
"tslib": "^2.6.2"
},
"engines": {
@@ -5147,7 +5482,6 @@
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.981.0.tgz",
"integrity": "sha512-zX3Xqm7V30J1D2II7WBL23SyqIIMD0wMzpiE+VosBxH6fAeXgrjIwSudCypNgnE1EK9OZoZMT3mJtkbUqUDdaA==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@aws-crypto/sha1-browser": "5.2.0",
"@aws-crypto/sha256-browser": "5.2.0",
@@ -5210,47 +5544,47 @@
}
},
"node_modules/@aws-sdk/client-sqs": {
- "version": "3.981.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.981.0.tgz",
- "integrity": "sha512-wURPt0vup9NE/S2Jdti+Mpe4PlaLFc1CInMCMIEua2/XAbX4MDrMBJ1ZvEKiAvgj/GgptI2gGnmbLHc7w78XrQ==",
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.986.0.tgz",
+ "integrity": "sha512-aYI5isAuqnHFv8oYTGCI0YfwF2fUhl2hSMewjY9je9ODP0irRYp7Zx1udkFtIg+bMizgIW32J3Xe6T1wCrUBpA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/credential-provider-node": "^3.972.4",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-node": "^3.972.6",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-sdk-sqs": "^3.972.5",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-sdk-sqs": "^3.972.6",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.981.0",
+ "@aws-sdk/util-endpoints": "3.986.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
+ "@smithy/core": "^3.22.1",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/md5-js": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -5261,46 +5595,62 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.986.0.tgz",
+ "integrity": "sha512-Mqi79L38qi1gCG3adlVdbNrSxvcm1IPDLiJPA3OBypY5ewxUyWbaA3DD4goG+EwET6LSFgZJcRSIh6KBNpP5pA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/client-ssm": {
- "version": "3.981.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.981.0.tgz",
- "integrity": "sha512-NOWbDJac1lHfknxqV+tPuNE9QxTCkH96VsWp8DnkPzxIOJJxCc/4fbzH+ugm6vZ67RICyRV1E8ZMMWhvFZrBfw==",
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.986.0.tgz",
+ "integrity": "sha512-90ZSt9LVAOhWntrdTH05STAQmSUnqkODdGcD7uS1kqoch3RxqaCXyi2CRI3OQnxlck82pJTGnCTuBRiIiUSTnw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/credential-provider-node": "^3.972.4",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-node": "^3.972.6",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.981.0",
+ "@aws-sdk/util-endpoints": "3.986.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
+ "@smithy/core": "^3.22.1",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -5312,45 +5662,61 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.986.0.tgz",
+ "integrity": "sha512-Mqi79L38qi1gCG3adlVdbNrSxvcm1IPDLiJPA3OBypY5ewxUyWbaA3DD4goG+EwET6LSFgZJcRSIh6KBNpP5pA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/client-sso": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.980.0.tgz",
- "integrity": "sha512-AhNXQaJ46C1I+lQ+6Kj+L24il5K9lqqIanJd8lMszPmP7bLnmX0wTKK0dxywcvrLdij3zhWttjAKEBNgLtS8/A==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.985.0.tgz",
+ "integrity": "sha512-81J8iE8MuXhdbMfIz4sWFj64Pe41bFi/uqqmqOC5SlGv+kwoyLsyKS/rH2tW2t5buih4vTUxskRjxlqikTD4oQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.7",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
+ "@aws-sdk/util-endpoints": "3.985.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
+ "@smithy/core": "^3.22.1",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -5362,9 +5728,9 @@
}
},
"node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
- "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz",
+ "integrity": "sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
@@ -5378,19 +5744,19 @@
}
},
"node_modules/@aws-sdk/core": {
- "version": "3.973.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.5.tgz",
- "integrity": "sha512-IMM7xGfLGW6lMvubsA4j6BHU5FPgGAxoQ/NA63KqNLMwTS+PeMBcx8DPHL12Vg6yqOZnqok9Mu4H2BdQyq7gSA==",
+ "version": "3.973.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.7.tgz",
+ "integrity": "sha512-wNZZQQNlJ+hzD49cKdo+PY6rsTDElO8yDImnrI69p2PLBa7QomeUKAJWYp9xnaR38nlHqWhMHZuYLCQ3oSX+xg==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/xml-builder": "^3.972.2",
- "@smithy/core": "^3.22.0",
+ "@aws-sdk/xml-builder": "^3.972.4",
+ "@smithy/core": "^3.22.1",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/signature-v4": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-middleware": "^4.2.8",
@@ -5415,12 +5781,12 @@
}
},
"node_modules/@aws-sdk/credential-provider-env": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.3.tgz",
- "integrity": "sha512-OBYNY4xQPq7Rx+oOhtyuyO0AQvdJSpXRg7JuPNBJH4a1XXIzJQl4UHQTPKZKwfJXmYLpv4+OkcFen4LYmDPd3g==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.5.tgz",
+ "integrity": "sha512-LxJ9PEO4gKPXzkufvIESUysykPIdrV7+Ocb9yAhbhJLE4TiAYqbCVUE+VuKP1leGR1bBfjWjYgSV5MxprlX3mQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.7",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/types": "^4.12.0",
@@ -5431,20 +5797,20 @@
}
},
"node_modules/@aws-sdk/credential-provider-http": {
- "version": "3.972.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.5.tgz",
- "integrity": "sha512-GpvBgEmSZPvlDekd26Zi+XsI27Qz7y0utUx0g2fSTSiDzhnd1FSa1owuodxR0BcUKNL7U2cOVhhDxgZ4iSoPVg==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.7.tgz",
+ "integrity": "sha512-L2uOGtvp2x3bTcxFTpSM+GkwFIPd8pHfGWO1764icMbo7e5xJh0nfhx1UwkXLnwvocTNEf8A7jISZLYjUSNaTg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.7",
"@aws-sdk/types": "^3.973.1",
"@smithy/fetch-http-handler": "^5.3.9",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
- "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-stream": "^4.5.11",
"tslib": "^2.6.2"
},
"engines": {
@@ -5452,19 +5818,19 @@
}
},
"node_modules/@aws-sdk/credential-provider-ini": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.3.tgz",
- "integrity": "sha512-rMQAIxstP7cLgYfsRGrGOlpyMl0l8JL2mcke3dsIPLWke05zKOFyR7yoJzWCsI/QiIxjRbxpvPiAeKEA6CoYkg==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.5.tgz",
+ "integrity": "sha512-SdDTYE6jkARzOeL7+kudMIM4DaFnP5dZVeatzw849k4bSXDdErDS188bgeNzc/RA2WGrlEpsqHUKP6G7sVXhZg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/credential-provider-env": "^3.972.3",
- "@aws-sdk/credential-provider-http": "^3.972.5",
- "@aws-sdk/credential-provider-login": "^3.972.3",
- "@aws-sdk/credential-provider-process": "^3.972.3",
- "@aws-sdk/credential-provider-sso": "^3.972.3",
- "@aws-sdk/credential-provider-web-identity": "^3.972.3",
- "@aws-sdk/nested-clients": "3.980.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-env": "^3.972.5",
+ "@aws-sdk/credential-provider-http": "^3.972.7",
+ "@aws-sdk/credential-provider-login": "^3.972.5",
+ "@aws-sdk/credential-provider-process": "^3.972.5",
+ "@aws-sdk/credential-provider-sso": "^3.972.5",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.5",
+ "@aws-sdk/nested-clients": "3.985.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/property-provider": "^4.2.8",
@@ -5477,13 +5843,13 @@
}
},
"node_modules/@aws-sdk/credential-provider-login": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.3.tgz",
- "integrity": "sha512-Gc3O91iVvA47kp2CLIXOwuo5ffo1cIpmmyIewcYjAcvurdFHQ8YdcBe1KHidnbbBO4/ZtywGBACsAX5vr3UdoA==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.5.tgz",
+ "integrity": "sha512-uYq1ILyTSI6ZDCMY5+vUsRM0SOCVI7kaW4wBrehVVkhAxC6y+e9rvGtnoZqCOWL1gKjTMouvsf4Ilhc5NCg1Aw==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/nested-clients": "3.980.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/nested-clients": "3.985.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/protocol-http": "^5.3.8",
@@ -5496,17 +5862,17 @@
}
},
"node_modules/@aws-sdk/credential-provider-node": {
- "version": "3.972.4",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.4.tgz",
- "integrity": "sha512-UwerdzosMSY7V5oIZm3NsMDZPv2aSVzSkZxYxIOWHBeKTZlUqW7XpHtJMZ4PZpJ+HMRhgP+MDGQx4THndgqJfQ==",
+ "version": "3.972.6",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.6.tgz",
+ "integrity": "sha512-DZ3CnAAtSVtVz+G+ogqecaErMLgzph4JH5nYbHoBMgBkwTUV+SUcjsjOJwdBJTHu3Dm6l5LBYekZoU2nDqQk2A==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/credential-provider-env": "^3.972.3",
- "@aws-sdk/credential-provider-http": "^3.972.5",
- "@aws-sdk/credential-provider-ini": "^3.972.3",
- "@aws-sdk/credential-provider-process": "^3.972.3",
- "@aws-sdk/credential-provider-sso": "^3.972.3",
- "@aws-sdk/credential-provider-web-identity": "^3.972.3",
+ "@aws-sdk/credential-provider-env": "^3.972.5",
+ "@aws-sdk/credential-provider-http": "^3.972.7",
+ "@aws-sdk/credential-provider-ini": "^3.972.5",
+ "@aws-sdk/credential-provider-process": "^3.972.5",
+ "@aws-sdk/credential-provider-sso": "^3.972.5",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.5",
"@aws-sdk/types": "^3.973.1",
"@smithy/credential-provider-imds": "^4.2.8",
"@smithy/property-provider": "^4.2.8",
@@ -5519,12 +5885,12 @@
}
},
"node_modules/@aws-sdk/credential-provider-process": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.3.tgz",
- "integrity": "sha512-xkSY7zjRqeVc6TXK2xr3z1bTLm0wD8cj3lAkproRGaO4Ku7dPlKy843YKnHrUOUzOnMezdZ4xtmFc0eKIDTo2w==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.5.tgz",
+ "integrity": "sha512-HDKF3mVbLnuqGg6dMnzBf1VUOywE12/N286msI9YaK9mEIzdsGCtLTvrDhe3Up0R9/hGFbB+9l21/TwF5L1C6g==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.7",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -5536,14 +5902,14 @@
}
},
"node_modules/@aws-sdk/credential-provider-sso": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.3.tgz",
- "integrity": "sha512-8Ww3F5Ngk8dZ6JPL/V5LhCU1BwMfQd3tLdoEuzaewX8FdnT633tPr+KTHySz9FK7fFPcz5qG3R5edVEhWQD4AA==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.5.tgz",
+ "integrity": "sha512-8urj3AoeNeQisjMmMBhFeiY2gxt6/7wQQbEGun0YV/OaOOiXrIudTIEYF8ZfD+NQI6X1FY5AkRsx6O/CaGiybA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/client-sso": "3.980.0",
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/token-providers": "3.980.0",
+ "@aws-sdk/client-sso": "3.985.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/token-providers": "3.985.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -5555,13 +5921,13 @@
}
},
"node_modules/@aws-sdk/credential-provider-web-identity": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.3.tgz",
- "integrity": "sha512-62VufdcH5rRfiRKZRcf1wVbbt/1jAntMj1+J0qAd+r5pQRg2t0/P9/Rz16B1o5/0Se9lVL506LRjrhIJAhYBfA==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.5.tgz",
+ "integrity": "sha512-OK3cULuJl6c+RcDZfPpaK5o3deTOnKZbxm7pzhFNGA3fI2hF9yDih17fGRazJzGGWaDVlR9ejZrpDef4DJCEsw==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/nested-clients": "3.980.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/nested-clients": "3.985.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -5572,6 +5938,23 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/dynamodb-codec": {
+ "version": "3.972.8",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.8.tgz",
+ "integrity": "sha512-5ngfn6fQPSNc7G9LlingK4SXfzcJtv5pOP++erc7HmCq0LcDj//0pcpLgxpDII0sBTh0FcR/iw9i4fBZwSJ2Cg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.7",
+ "@smithy/core": "^3.22.1",
+ "@smithy/smithy-client": "^4.11.2",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-base64": "^4.3.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/endpoint-cache": {
"version": "3.972.2",
"resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.2.tgz",
@@ -5605,27 +5988,6 @@
"@aws-sdk/client-dynamodb": "3.981.0"
}
},
- "node_modules/@aws-sdk/lib-storage": {
- "version": "3.981.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.981.0.tgz",
- "integrity": "sha512-uNkWK1WthkgVglOSgu+0hjo/yVko73E7TbAL4BlAUYOG0AJZ105f0sJugoBILNgLrlRf70VQaPTjLGuUUQk8HQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/abort-controller": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/smithy-client": "^4.11.1",
- "buffer": "5.6.0",
- "events": "3.3.0",
- "stream-browserify": "3.0.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- },
- "peerDependencies": {
- "@aws-sdk/client-s3": "3.981.0"
- }
- },
"node_modules/@aws-sdk/middleware-bucket-endpoint": {
"version": "3.972.3",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz",
@@ -5677,15 +6039,15 @@
}
},
"node_modules/@aws-sdk/middleware-flexible-checksums": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.3.tgz",
- "integrity": "sha512-MkNGJ6qB9kpsLwL18kC/ZXppsJbftHVGCisqpEVbTQsum8CLYDX1Bmp/IvhRGNxsqCO2w9/4PwhDKBjG3Uvr4Q==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.5.tgz",
+ "integrity": "sha512-SF/1MYWx67OyCrLA4icIpWUfCkdlOi8Y1KecQ9xYxkL10GMjVdPTGPnYhAg0dw5U43Y9PVUWhAV2ezOaG+0BLg==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/crc32": "5.2.0",
"@aws-crypto/crc32c": "5.2.0",
"@aws-crypto/util": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.7",
"@aws-sdk/crc64-nvme": "3.972.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/is-array-buffer": "^4.2.0",
@@ -5693,7 +6055,7 @@
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
"@smithy/util-middleware": "^4.2.8",
- "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-stream": "^4.5.11",
"@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
@@ -5761,23 +6123,23 @@
}
},
"node_modules/@aws-sdk/middleware-sdk-s3": {
- "version": "3.972.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.5.tgz",
- "integrity": "sha512-3IgeIDiQ15tmMBFIdJ1cTy3A9rXHGo+b9p22V38vA3MozeMyVC8VmCYdDLA0iMWo4VHA9LDJTgCM0+xU3wjBOg==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.7.tgz",
+ "integrity": "sha512-VtZ7tMIw18VzjG+I6D6rh2eLkJfTtByiFoCIauGDtTTPBEUMQUiGaJ/zZrPlCY6BsvLLeFKz3+E5mntgiOWmIg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.7",
"@aws-sdk/types": "^3.973.1",
"@aws-sdk/util-arn-parser": "^3.972.2",
- "@smithy/core": "^3.22.0",
+ "@smithy/core": "^3.22.1",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/protocol-http": "^5.3.8",
"@smithy/signature-v4": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/util-config-provider": "^4.2.0",
"@smithy/util-middleware": "^4.2.8",
- "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-stream": "^4.5.11",
"@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
@@ -5786,13 +6148,13 @@
}
},
"node_modules/@aws-sdk/middleware-sdk-sqs": {
- "version": "3.972.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.5.tgz",
- "integrity": "sha512-TnGzPJ9dPLqDltOaM0depE4VpAX3FS6xgJXBe2nigLUy9MMwovFGXzw/eGjAg1sDSVxfQ9EpbNkmyBcCoDQ74g==",
+ "version": "3.972.6",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.6.tgz",
+ "integrity": "sha512-6e+dZ1qPEIDO8ASIu09QNVrwXAzuLOuD5jd1M7oj41e+/wShJPn2oG8ZDYUGTnGBOrc4h1UILWYodzMzzTrkiQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/util-hex-encoding": "^4.2.0",
"@smithy/util-utf8": "^4.2.0",
@@ -5817,15 +6179,15 @@
}
},
"node_modules/@aws-sdk/middleware-user-agent": {
- "version": "3.972.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.5.tgz",
- "integrity": "sha512-TVZQ6PWPwQbahUI8V+Er+gS41ctIawcI/uMNmQtQ7RMcg3JYn6gyKAFKUb3HFYx2OjYlx1u11sETSwwEUxVHTg==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.7.tgz",
+ "integrity": "sha512-HUD+geASjXSCyL/DHPQc/Ua7JhldTcIglVAoCV8kiVm99IaFSlAbTvEnyhZwdE6bdFyTL+uIaWLaCFSRsglZBQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.7",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
- "@smithy/core": "^3.22.0",
+ "@aws-sdk/util-endpoints": "3.985.0",
+ "@smithy/core": "^3.22.1",
"@smithy/protocol-http": "^5.3.8",
"@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
@@ -5835,9 +6197,9 @@
}
},
"node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
- "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz",
+ "integrity": "sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
@@ -5851,44 +6213,44 @@
}
},
"node_modules/@aws-sdk/nested-clients": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz",
- "integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.985.0.tgz",
+ "integrity": "sha512-TsWwKzb/2WHafAY0CE7uXgLj0FmnkBTgfioG9HO+7z/zCPcl1+YU+i7dW4o0y+aFxFgxTMG+ExBQpqT/k2ao8g==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
+ "@aws-sdk/core": "^3.973.7",
"@aws-sdk/middleware-host-header": "^3.972.3",
"@aws-sdk/middleware-logger": "^3.972.3",
"@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
+ "@aws-sdk/util-endpoints": "3.985.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
+ "@smithy/core": "^3.22.1",
"@smithy/fetch-http-handler": "^5.3.9",
"@smithy/hash-node": "^4.2.8",
"@smithy/invalid-dependency": "^4.2.8",
"@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
"@smithy/middleware-serde": "^4.2.9",
"@smithy/middleware-stack": "^4.2.8",
"@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/node-http-handler": "^4.4.9",
"@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
+ "@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
"@smithy/url-parser": "^4.2.8",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
"@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
"@smithy/util-endpoints": "^3.2.8",
"@smithy/util-middleware": "^4.2.8",
"@smithy/util-retry": "^4.2.8",
@@ -5900,9 +6262,9 @@
}
},
"node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
- "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz",
+ "integrity": "sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
@@ -5949,13 +6311,13 @@
}
},
"node_modules/@aws-sdk/token-providers": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.980.0.tgz",
- "integrity": "sha512-1nFileg1wAgDmieRoj9dOawgr2hhlh7xdvcH57b1NnqfPaVlcqVJyPc6k3TLDUFPY69eEwNxdGue/0wIz58vjA==",
+ "version": "3.985.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.985.0.tgz",
+ "integrity": "sha512-+hwpHZyEq8k+9JL2PkE60V93v2kNhUIv7STFt+EAez1UJsJOQDhc5LpzEX66pNjclI5OTwBROs/DhJjC/BtMjQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/nested-clients": "3.980.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/nested-clients": "3.985.0",
"@aws-sdk/types": "^3.973.1",
"@smithy/property-provider": "^4.2.8",
"@smithy/shared-ini-file-loader": "^4.4.3",
@@ -6047,12 +6409,12 @@
}
},
"node_modules/@aws-sdk/util-user-agent-node": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.3.tgz",
- "integrity": "sha512-gqG+02/lXQtO0j3US6EVnxtwwoXQC5l2qkhLCrqUrqdtcQxV7FDMbm9wLjKqoronSHyELGTjbFKK/xV5q1bZNA==",
+ "version": "3.972.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.5.tgz",
+ "integrity": "sha512-GsUDF+rXyxDZkkJxUsDxnA67FG+kc5W1dnloCFLl6fWzceevsCYzJpASBzT+BPjwUgREE6FngfJYYYMQUY5fZQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/middleware-user-agent": "^3.972.5",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/types": "^3.973.1",
"@smithy/node-config-provider": "^4.3.8",
"@smithy/types": "^4.12.0",
@@ -6071,9 +6433,9 @@
}
},
"node_modules/@aws-sdk/xml-builder": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.3.tgz",
- "integrity": "sha512-bCk63RsBNCWW4tt5atv5Sbrh+3J3e8YzgyF6aZb1JeXcdzG4k5SlPLeTMFOIXFuuFHIwgphUhn4i3uS/q49eww==",
+ "version": "3.972.4",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz",
+ "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.12.0",
@@ -20219,6 +20581,10 @@
"resolved": "lambdas/report-event-transformer",
"link": true
},
+ "node_modules/nhs-notify-digital-letters-report-generator": {
+ "resolved": "lambdas/report-generator",
+ "link": true
+ },
"node_modules/nhs-notify-digital-letters-report-scheduler-lambda": {
"resolved": "lambdas/report-scheduler",
"link": true
@@ -24825,14 +25191,15 @@
"utils/utils": {
"version": "0.0.1",
"dependencies": {
- "@aws-sdk/client-dynamodb": "^3.914.0",
- "@aws-sdk/client-eventbridge": "^3.918.0",
- "@aws-sdk/client-lambda": "^3.914.0",
- "@aws-sdk/client-s3": "^3.914.0",
- "@aws-sdk/client-sqs": "^3.914.0",
- "@aws-sdk/client-ssm": "^3.914.0",
- "@aws-sdk/lib-dynamodb": "^3.914.0",
- "@aws-sdk/lib-storage": "^3.914.0",
+ "@aws-sdk/client-athena": "^3.984.0",
+ "@aws-sdk/client-dynamodb": "^3.984.0",
+ "@aws-sdk/client-eventbridge": "^3.984.0",
+ "@aws-sdk/client-lambda": "^3.984.0",
+ "@aws-sdk/client-s3": "^3.984.0",
+ "@aws-sdk/client-sqs": "^3.984.0",
+ "@aws-sdk/client-ssm": "^3.984.0",
+ "@aws-sdk/lib-dynamodb": "^3.984.0",
+ "@aws-sdk/lib-storage": "^3.984.0",
"async-mutex": "^0.4.0",
"axios": "^1.13.2",
"date-fns": "^4.1.0",
@@ -24854,6 +25221,216 @@
"typescript": "^5.8.2"
}
},
+ "utils/utils/node_modules/@aws-sdk/client-dynamodb": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.986.0.tgz",
+ "integrity": "sha512-4SPBE+QzRl8Yi8mSHDahwE+rKgCB1RhiIYoeqfwpkiocXnMaBQsNSEaJaschLTbC6cOPs2RqM1/oIF50do/OvA==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-node": "^3.972.6",
+ "@aws-sdk/dynamodb-codec": "^3.972.8",
+ "@aws-sdk/middleware-endpoint-discovery": "^3.972.3",
+ "@aws-sdk/middleware-host-header": "^3.972.3",
+ "@aws-sdk/middleware-logger": "^3.972.3",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.3",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
+ "@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.986.0",
+ "@aws-sdk/util-user-agent-browser": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/core": "^3.22.1",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/hash-node": "^4.2.8",
+ "@smithy/invalid-dependency": "^4.2.8",
+ "@smithy/middleware-content-length": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/node-http-handler": "^4.4.9",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.11.2",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/util-utf8": "^4.2.0",
+ "@smithy/util-waiter": "^4.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "utils/utils/node_modules/@aws-sdk/client-s3": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.986.0.tgz",
+ "integrity": "sha512-IcDJ8shVVvbxgMe8+dLWcv6uhSwmX65PHTVGX81BhWAElPnp3CL8w/5uzOPRo4n4/bqIk9eskGVEIicw2o+SrA==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@aws-crypto/sha1-browser": "5.2.0",
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-node": "^3.972.6",
+ "@aws-sdk/middleware-bucket-endpoint": "^3.972.3",
+ "@aws-sdk/middleware-expect-continue": "^3.972.3",
+ "@aws-sdk/middleware-flexible-checksums": "^3.972.5",
+ "@aws-sdk/middleware-host-header": "^3.972.3",
+ "@aws-sdk/middleware-location-constraint": "^3.972.3",
+ "@aws-sdk/middleware-logger": "^3.972.3",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.3",
+ "@aws-sdk/middleware-sdk-s3": "^3.972.7",
+ "@aws-sdk/middleware-ssec": "^3.972.3",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
+ "@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/signature-v4-multi-region": "3.986.0",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.986.0",
+ "@aws-sdk/util-user-agent-browser": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/core": "^3.22.1",
+ "@smithy/eventstream-serde-browser": "^4.2.8",
+ "@smithy/eventstream-serde-config-resolver": "^4.3.8",
+ "@smithy/eventstream-serde-node": "^4.2.8",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/hash-blob-browser": "^4.2.9",
+ "@smithy/hash-node": "^4.2.8",
+ "@smithy/hash-stream-node": "^4.2.8",
+ "@smithy/invalid-dependency": "^4.2.8",
+ "@smithy/md5-js": "^4.2.8",
+ "@smithy/middleware-content-length": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/node-http-handler": "^4.4.9",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.11.2",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/util-stream": "^4.5.11",
+ "@smithy/util-utf8": "^4.2.0",
+ "@smithy/util-waiter": "^4.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "utils/utils/node_modules/@aws-sdk/lib-dynamodb": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.986.0.tgz",
+ "integrity": "sha512-1FNV5/hl45Ao4tW+SOU5Gymyb3x9e+xdYXRHSyXvjKBF0WsFqWBnMIhGFFDum0Uv9tdte3hR8U07oU3gDgSJIw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/util-dynamodb": "3.986.0",
+ "@smithy/core": "^3.22.1",
+ "@smithy/smithy-client": "^4.11.2",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/client-dynamodb": "^3.986.0"
+ }
+ },
+ "utils/utils/node_modules/@aws-sdk/lib-storage": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.986.0.tgz",
+ "integrity": "sha512-tcP8NmpBidRHNhGqRQWEHG1vEsMTjOLd28aIS8dfhaFIAxFGMe5CH+fXunYE5ie52NZ5pLUrCvtAnwfBfSBbUw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/abort-controller": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/smithy-client": "^4.11.2",
+ "buffer": "5.6.0",
+ "events": "3.3.0",
+ "stream-browserify": "3.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/client-s3": "^3.986.0"
+ }
+ },
+ "utils/utils/node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.986.0.tgz",
+ "integrity": "sha512-Upw+rw7wCH93E6QWxqpAqJLrUmJYVUAWrk4tCOBnkeuwzGERZvJFL5UQ6TAJFj9T18Ih+vNFaACh8J5aP4oTBw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-sdk-s3": "^3.972.7",
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/signature-v4": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "utils/utils/node_modules/@aws-sdk/util-dynamodb": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.986.0.tgz",
+ "integrity": "sha512-BSDdaOm2r9fDR6N1SpZTgEkJ86/wzQcnHHgISF50hXxApilRKBXZ+5siOr9yEiYcEXr3tDng7XUeny1pV3axlA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/client-dynamodb": "^3.986.0"
+ }
+ },
+ "utils/utils/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.986.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.986.0.tgz",
+ "integrity": "sha512-Mqi79L38qi1gCG3adlVdbNrSxvcm1IPDLiJPA3OBypY5ewxUyWbaA3DD4goG+EwET6LSFgZJcRSIh6KBNpP5pA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"utils/utils/node_modules/@jest/core": {
"version": "29.7.0",
"dev": true,
diff --git a/package.json b/package.json
index 76aeeb2e..2445bb27 100644
--- a/package.json
+++ b/package.json
@@ -70,6 +70,7 @@
"lambdas/report-scheduler",
"lambdas/report-event-transformer",
"lambdas/move-scanned-files-lambda",
+ "lambdas/report-generator",
"utils/utils",
"utils/sender-management",
"src/cloudevents",
diff --git a/utils/utils/package.json b/utils/utils/package.json
index 5669cae8..d51a4306 100644
--- a/utils/utils/package.json
+++ b/utils/utils/package.json
@@ -1,13 +1,14 @@
{
"dependencies": {
- "@aws-sdk/client-dynamodb": "^3.914.0",
- "@aws-sdk/client-eventbridge": "^3.918.0",
- "@aws-sdk/client-lambda": "^3.914.0",
- "@aws-sdk/client-s3": "^3.914.0",
- "@aws-sdk/client-sqs": "^3.914.0",
- "@aws-sdk/client-ssm": "^3.914.0",
- "@aws-sdk/lib-dynamodb": "^3.914.0",
- "@aws-sdk/lib-storage": "^3.914.0",
+ "@aws-sdk/client-athena": "^3.984.0",
+ "@aws-sdk/client-dynamodb": "^3.984.0",
+ "@aws-sdk/client-eventbridge": "^3.984.0",
+ "@aws-sdk/client-lambda": "^3.984.0",
+ "@aws-sdk/client-s3": "^3.984.0",
+ "@aws-sdk/client-sqs": "^3.984.0",
+ "@aws-sdk/client-ssm": "^3.984.0",
+ "@aws-sdk/lib-dynamodb": "^3.984.0",
+ "@aws-sdk/lib-storage": "^3.984.0",
"async-mutex": "^0.4.0",
"axios": "^1.13.2",
"date-fns": "^4.1.0",
diff --git a/utils/utils/src/__tests__/reporting/data-repository.test.ts b/utils/utils/src/__tests__/reporting/data-repository.test.ts
new file mode 100644
index 00000000..4a9f13c2
--- /dev/null
+++ b/utils/utils/src/__tests__/reporting/data-repository.test.ts
@@ -0,0 +1,152 @@
+import { AthenaClient } from '@aws-sdk/client-athena';
+import { mockClient } from 'aws-sdk-client-mock';
+import { AthenaRepository } from '../../reporting/data-repository';
+
+const athenaClientMock = mockClient(AthenaClient);
+
+describe('AthenaRepository', () => {
+ let repository: AthenaRepository;
+ const mockConfig = {
+ athenaWorkgroup: 'test-workgroup',
+ athenaDatabase: 'test-database',
+ };
+
+ beforeEach(() => {
+ athenaClientMock.reset();
+ repository = new AthenaRepository(new AthenaClient({}), mockConfig);
+ });
+
+ describe('constructor', () => {
+ it('should initialize with correct workgroup and database', () => {
+ expect(repository.workGroup).toBe('test-workgroup');
+ expect(repository.database).toBe('test-database');
+ });
+ });
+
+ describe('startQuery', () => {
+ it('should start query execution and return query execution ID', async () => {
+ const mockQueryExecutionId = 'query-123';
+ athenaClientMock.onAnyCommand().resolves({
+ QueryExecutionId: mockQueryExecutionId,
+ });
+
+ const result = await repository.startQuery('SELECT * FROM table', [
+ 'param1',
+ 'param2',
+ ]);
+
+ expect(result).toBe(mockQueryExecutionId);
+ });
+
+ it('should send correct parameters to Athena client', async () => {
+ const query = 'SELECT * FROM table WHERE id = ?';
+ const executionParameters = ['123'];
+
+ athenaClientMock.onAnyCommand().resolves({
+ QueryExecutionId: 'query-456',
+ });
+
+ await repository.startQuery(query, executionParameters);
+
+ const calls = athenaClientMock.commandCalls(
+ Object.getPrototypeOf(athenaClientMock.calls()[0].args[0]).constructor,
+ );
+ expect(calls[0].args[0].input).toEqual({
+ QueryString: query,
+ WorkGroup: 'test-workgroup',
+ QueryExecutionContext: { Database: 'test-database' },
+ ExecutionParameters: executionParameters,
+ });
+ });
+
+ it('should handle empty execution parameters', async () => {
+ athenaClientMock.onAnyCommand().resolves({
+ QueryExecutionId: 'query-789',
+ });
+
+ const result = await repository.startQuery('SELECT 1', []);
+
+ expect(result).toBe('query-789');
+ });
+
+ it('should propagate Athena client errors', async () => {
+ const mockError = new Error('Athena service error');
+ athenaClientMock.onAnyCommand().rejects(mockError);
+
+ await expect(
+ repository.startQuery('SELECT * FROM table', []),
+ ).rejects.toThrow('Athena service error');
+ });
+ });
+
+ describe('getQueryStatus', () => {
+ it('should return query execution state', async () => {
+ athenaClientMock.onAnyCommand().resolves({
+ QueryExecution: {
+ Status: {
+ State: 'SUCCEEDED',
+ },
+ },
+ });
+
+ const result = await repository.getQueryStatus('query-123');
+
+ expect(result).toBe('SUCCEEDED');
+ });
+
+ it('should handle RUNNING state', async () => {
+ athenaClientMock.onAnyCommand().resolves({
+ QueryExecution: {
+ Status: {
+ State: 'RUNNING',
+ },
+ },
+ });
+
+ const result = await repository.getQueryStatus('query-456');
+
+ expect(result).toBe('RUNNING');
+ });
+
+ it('should handle FAILED state', async () => {
+ athenaClientMock.onAnyCommand().resolves({
+ QueryExecution: {
+ Status: {
+ State: 'FAILED',
+ },
+ },
+ });
+
+ const result = await repository.getQueryStatus('query-789');
+
+ expect(result).toBe('FAILED');
+ });
+
+ it('should return undefined when QueryExecution is missing', async () => {
+ athenaClientMock.onAnyCommand().resolves({});
+
+ const result = await repository.getQueryStatus('query-999');
+
+ expect(result).toBeUndefined();
+ });
+
+ it('should return undefined when Status is missing', async () => {
+ athenaClientMock.onAnyCommand().resolves({
+ QueryExecution: {},
+ });
+
+ const result = await repository.getQueryStatus('query-888');
+
+ expect(result).toBeUndefined();
+ });
+
+ it('should propagate Athena client errors', async () => {
+ const mockError = new Error('Query not found');
+ athenaClientMock.onAnyCommand().rejects(mockError);
+
+ await expect(repository.getQueryStatus('invalid-query')).rejects.toThrow(
+ 'Query not found',
+ );
+ });
+ });
+});
diff --git a/utils/utils/src/__tests__/reporting/report-service.test.ts b/utils/utils/src/__tests__/reporting/report-service.test.ts
new file mode 100644
index 00000000..8f84424e
--- /dev/null
+++ b/utils/utils/src/__tests__/reporting/report-service.test.ts
@@ -0,0 +1,201 @@
+import { Logger } from '../../logger';
+import { ReportService } from '../../reporting/report-service';
+import { IDataRepository } from '../../reporting/data-repository';
+import { IStorageRepository } from '../../reporting/storage-repository';
+import { sleep } from '../../util-retry/sleep';
+
+jest.mock('../../util-retry/sleep');
+
+describe('ReportService', () => {
+ let mockDataRepository: jest.Mocked;
+ let mockStorageRepository: jest.Mocked;
+ let mockLogger: jest.Mocked;
+ let reportService: ReportService;
+
+ const defaultMaxPollLimit = 10;
+ const defaultWaitForInSeconds = 1;
+
+ beforeEach(() => {
+ mockDataRepository = {
+ startQuery: jest.fn(),
+ getQueryStatus: jest.fn(),
+ } as jest.Mocked;
+
+ mockStorageRepository = {
+ publishReport: jest.fn(),
+ } as jest.Mocked;
+
+ mockLogger = {
+ child: jest.fn().mockReturnThis(),
+ info: jest.fn(),
+ error: jest.fn(),
+ } as unknown as jest.Mocked;
+
+ reportService = new ReportService(
+ mockDataRepository,
+ mockStorageRepository,
+ defaultMaxPollLimit,
+ defaultWaitForInSeconds,
+ mockLogger,
+ );
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('generateReport', () => {
+ const query = 'SELECT * FROM test_table';
+ const executionParameters = ['param1', 'param2'];
+ const reportFilePath = 's3://bucket/report.csv';
+ const queryExecutionId = 'test-execution-id-123';
+
+ it('should generate report successfully when query succeeds', async () => {
+ mockDataRepository.startQuery.mockResolvedValue(queryExecutionId);
+ mockDataRepository.getQueryStatus.mockResolvedValue('SUCCEEDED');
+ mockStorageRepository.publishReport.mockResolvedValue(reportFilePath);
+
+ const result = await reportService.generateReport(
+ query,
+ executionParameters,
+ reportFilePath,
+ );
+
+ expect(mockDataRepository.startQuery).toHaveBeenCalledWith(
+ query,
+ executionParameters,
+ );
+ expect(mockDataRepository.getQueryStatus).toHaveBeenCalledWith(
+ queryExecutionId,
+ );
+ expect(mockStorageRepository.publishReport).toHaveBeenCalledWith(
+ queryExecutionId,
+ reportFilePath,
+ );
+ expect(result).toBe(reportFilePath);
+ expect(mockLogger.child).toHaveBeenCalledWith({ queryExecutionId });
+ expect(mockLogger.info).toHaveBeenCalledWith(
+ `Athena query started with execution id: ${queryExecutionId}`,
+ );
+ expect(mockLogger.info).toHaveBeenCalledWith(
+ `Athena query ${queryExecutionId} finished`,
+ );
+ });
+
+ it('should throw error when query fails', async () => {
+ mockDataRepository.startQuery.mockResolvedValue(queryExecutionId);
+ mockDataRepository.getQueryStatus.mockResolvedValue('FAILED');
+
+ await expect(
+ reportService.generateReport(
+ query,
+ executionParameters,
+ reportFilePath,
+ ),
+ ).rejects.toThrow('Failed to generate report. Query status: FAILED');
+
+ expect(mockStorageRepository.publishReport).not.toHaveBeenCalled();
+ });
+
+ it('should throw error when query is cancelled', async () => {
+ mockDataRepository.startQuery.mockResolvedValue(queryExecutionId);
+ mockDataRepository.getQueryStatus.mockResolvedValue('CANCELLED');
+
+ await expect(
+ reportService.generateReport(
+ query,
+ executionParameters,
+ reportFilePath,
+ ),
+ ).rejects.toThrow('Failed to generate report. Query status: CANCELLED');
+ });
+
+ it('should poll until query succeeds', async () => {
+ mockDataRepository.startQuery.mockResolvedValue(queryExecutionId);
+ mockDataRepository.getQueryStatus
+ .mockResolvedValueOnce('QUEUED')
+ .mockResolvedValueOnce('RUNNING')
+ .mockResolvedValueOnce('RUNNING')
+ .mockResolvedValueOnce('SUCCEEDED');
+ mockStorageRepository.publishReport.mockResolvedValue(reportFilePath);
+
+ await reportService.generateReport(
+ query,
+ executionParameters,
+ reportFilePath,
+ );
+
+ expect(mockDataRepository.getQueryStatus).toHaveBeenCalledTimes(4);
+ expect(sleep).toHaveBeenCalledTimes(4);
+ expect(sleep).toHaveBeenCalledWith(defaultWaitForInSeconds);
+ });
+
+ it('should throw error when max poll limit is reached', async () => {
+ const shortPollLimit = 3;
+ const shortReportService = new ReportService(
+ mockDataRepository,
+ mockStorageRepository,
+ shortPollLimit,
+ defaultWaitForInSeconds,
+ mockLogger,
+ );
+
+ mockDataRepository.startQuery.mockResolvedValue(queryExecutionId);
+ mockDataRepository.getQueryStatus.mockResolvedValue('RUNNING');
+
+ await expect(
+ shortReportService.generateReport(
+ query,
+ executionParameters,
+ reportFilePath,
+ ),
+ ).rejects.toThrow('Failed to generate report. Query status: RUNNING');
+
+ expect(mockDataRepository.getQueryStatus).toHaveBeenCalledTimes(
+ shortPollLimit,
+ );
+ });
+
+ it('should handle UNKNOWN status and continue polling', async () => {
+ mockDataRepository.startQuery.mockResolvedValue(queryExecutionId);
+ mockDataRepository.getQueryStatus
+ .mockResolvedValueOnce('QUEUED')
+ .mockResolvedValueOnce('UNKNOWN')
+ .mockResolvedValueOnce('SUCCEEDED');
+ mockStorageRepository.publishReport.mockResolvedValue(reportFilePath);
+
+ await reportService.generateReport(
+ query,
+ executionParameters,
+ reportFilePath,
+ );
+
+ expect(mockDataRepository.getQueryStatus).toHaveBeenCalledTimes(3);
+ });
+
+ it('should respect custom wait time between polls', async () => {
+ const customWaitTime = 5;
+ const customReportService = new ReportService(
+ mockDataRepository,
+ mockStorageRepository,
+ defaultMaxPollLimit,
+ customWaitTime,
+ mockLogger,
+ );
+
+ mockDataRepository.startQuery.mockResolvedValue(queryExecutionId);
+ mockDataRepository.getQueryStatus
+ .mockResolvedValueOnce('RUNNING')
+ .mockResolvedValueOnce('SUCCEEDED');
+ mockStorageRepository.publishReport.mockResolvedValue(reportFilePath);
+
+ await customReportService.generateReport(
+ query,
+ executionParameters,
+ reportFilePath,
+ );
+
+ expect(sleep).toHaveBeenCalledWith(customWaitTime);
+ });
+ });
+});
diff --git a/utils/utils/src/__tests__/reporting/storage-repository.test.ts b/utils/utils/src/__tests__/reporting/storage-repository.test.ts
new file mode 100644
index 00000000..4fb65e96
--- /dev/null
+++ b/utils/utils/src/__tests__/reporting/storage-repository.test.ts
@@ -0,0 +1,76 @@
+import { CopyObjectCommand, S3Client } from '@aws-sdk/client-s3';
+import { mockClient } from 'aws-sdk-client-mock';
+import { createStorageRepository } from '../../reporting/storage-repository';
+import { Logger } from '../../logger';
+
+const s3Mock = mockClient(S3Client);
+
+describe('StorageRepository', () => {
+ const mockLogger = {
+ debug: jest.fn(),
+ error: jest.fn(),
+ info: jest.fn(),
+ warn: jest.fn(),
+ } as unknown as jest.Mocked;
+ const reportingBucketName = 'test-reporting-bucket';
+ let storageRepository: ReturnType;
+
+ beforeEach(() => {
+ s3Mock.reset();
+ storageRepository = createStorageRepository({
+ s3Client: new S3Client({}),
+ reportingBucketName,
+ logger: mockLogger,
+ });
+ });
+
+ describe('publishReport', () => {
+ it('should copy report from athena-output to target path', async () => {
+ const reportQueryId = 'query-123';
+ const reportFilePath = 'reports/2024/report.csv';
+
+ s3Mock.on(CopyObjectCommand).resolves({});
+
+ const result = await storageRepository.publishReport(
+ reportQueryId,
+ reportFilePath,
+ );
+
+ expect(result).toBe(`s3://${reportingBucketName}/${reportFilePath}`);
+ expect(s3Mock.calls()).toHaveLength(1);
+
+ const copyCommand = s3Mock.call(0).args[0].input;
+ expect(copyCommand).toEqual({
+ CopySource: `${reportingBucketName}/athena-output/${reportQueryId}.csv`,
+ Bucket: reportingBucketName,
+ Key: reportFilePath,
+ });
+ });
+
+ it('should throw error when S3 copy fails', async () => {
+ const reportQueryId = 'query-456';
+ const reportFilePath = 'reports/2024/failed-report.csv';
+ const s3Error = new Error('S3 CopyObject failed');
+
+ s3Mock.on(CopyObjectCommand).rejects(s3Error);
+
+ await expect(
+ storageRepository.publishReport(reportQueryId, reportFilePath),
+ ).rejects.toThrow('S3 CopyObject failed');
+ });
+
+ it('should construct correct S3 URIs for nested paths', async () => {
+ const reportQueryId = 'query-789';
+ const reportFilePath = 'reports/2024/01/daily/report.csv';
+
+ s3Mock.on(CopyObjectCommand).resolves({});
+
+ const result = await storageRepository.publishReport(
+ reportQueryId,
+ reportFilePath,
+ );
+
+ expect(result).toBe(`s3://${reportingBucketName}/${reportFilePath}`);
+ });
+ });
+});
diff --git a/utils/utils/src/index.ts b/utils/utils/src/index.ts
index 8da5a845..4b9333bc 100644
--- a/utils/utils/src/index.ts
+++ b/utils/utils/src/index.ts
@@ -15,3 +15,4 @@ export * from './event-bridge-utils';
export * from './key-generation-utils';
export * from './schema-utils';
export * from './pdm-client';
+export * from './reporting';
diff --git a/utils/utils/src/reporting/data-repository.ts b/utils/utils/src/reporting/data-repository.ts
new file mode 100644
index 00000000..da7bbad5
--- /dev/null
+++ b/utils/utils/src/reporting/data-repository.ts
@@ -0,0 +1,71 @@
+import {
+ AthenaClient,
+ GetQueryExecutionCommand,
+ StartQueryExecutionCommand,
+} from '@aws-sdk/client-athena';
+import type { Logger } from '../logger';
+
+export type DataRepositoryDependencies = {
+ athenaClient: AthenaClient;
+ config: Record;
+ logger: Logger;
+};
+
+export type IDataRepository = {
+ startQuery(
+ query: string,
+ executionParameters: string[],
+ ): Promise;
+ getQueryStatus(reportQueryId: string): Promise;
+};
+
+export class AthenaRepository implements IDataRepository {
+ readonly athenaClient: AthenaClient;
+
+ readonly workGroup: string;
+
+ readonly database: string;
+
+ constructor(athenaClient: AthenaClient, config: Record) {
+ this.athenaClient = athenaClient;
+ this.workGroup = config.athenaWorkgroup;
+ this.database = config.athenaDatabase;
+ }
+
+ /**
+ * Asynchronously starts a query execution in Athena.
+ *
+ * @param {string} query - The query string to execute.
+ * @return {Promise} - The ID of the query execution.
+ */
+ async startQuery(query: string, executionParameters: string[]) {
+ const executionCommand = new StartQueryExecutionCommand({
+ QueryString: query,
+ WorkGroup: this.workGroup,
+ QueryExecutionContext: { Database: this.database },
+ ExecutionParameters: executionParameters,
+ });
+
+ const { QueryExecutionId } = await this.athenaClient.send(executionCommand);
+
+ return QueryExecutionId;
+ }
+
+ /**
+ * Retrieves the status of a query execution.
+ *
+ * @param {string} reportQueryId - The ID of the query execution.
+ * @return {State} The state of the query execution.
+ */
+ async getQueryStatus(reportQueryId: string) {
+ const getQueryExecutionCommand = new GetQueryExecutionCommand({
+ QueryExecutionId: reportQueryId,
+ });
+
+ const { QueryExecution } = await this.athenaClient.send(
+ getQueryExecutionCommand,
+ );
+
+ return QueryExecution?.Status?.State;
+ }
+}
diff --git a/utils/utils/src/reporting/index.ts b/utils/utils/src/reporting/index.ts
new file mode 100644
index 00000000..c89877d1
--- /dev/null
+++ b/utils/utils/src/reporting/index.ts
@@ -0,0 +1,3 @@
+export * from './data-repository';
+export * from './report-service';
+export * from './storage-repository';
diff --git a/utils/utils/src/reporting/report-service.ts b/utils/utils/src/reporting/report-service.ts
new file mode 100644
index 00000000..c92a36e1
--- /dev/null
+++ b/utils/utils/src/reporting/report-service.ts
@@ -0,0 +1,96 @@
+import type { Logger } from '../logger';
+import { sleep } from '../util-retry/sleep';
+import { IDataRepository } from './data-repository';
+import { IStorageRepository } from './storage-repository';
+
+export interface IReportService {
+ generateReport(
+ query: string,
+ executionParameters: string[],
+ reportFilePath: string,
+ ): Promise;
+}
+
+export class ReportService implements IReportService {
+ readonly dataRepository: IDataRepository;
+
+ readonly storageRepository: IStorageRepository;
+
+ readonly maxPollLimit: number;
+
+ readonly waitForInSeconds: number;
+
+ readonly logger: Logger;
+
+ constructor(
+ dataRepository: IDataRepository,
+ storageRepository: IStorageRepository,
+ maxPollLimit: number,
+ waitForInSeconds: number,
+ logger: Logger,
+ ) {
+ this.dataRepository = dataRepository;
+ this.storageRepository = storageRepository;
+ this.maxPollLimit = maxPollLimit;
+ this.waitForInSeconds = waitForInSeconds;
+ this.logger = logger;
+ }
+
+ async generateReport(
+ query: string,
+ executionParameters: string[],
+ reportFilePath: string,
+ ): Promise {
+ const queryExecutionId = await this.dataRepository.startQuery(
+ query,
+ executionParameters,
+ );
+
+ if (!queryExecutionId) {
+ throw new Error('failed to obtained a query executionId from Athena');
+ }
+
+ const logger = this.logger.child({ queryExecutionId });
+
+ logger.info(`Athena query started with execution id: ${queryExecutionId}`);
+
+ const status = await this.poll(
+ queryExecutionId,
+ this.maxPollLimit,
+ this.waitForInSeconds,
+ );
+
+ if (status !== 'SUCCEEDED') {
+ throw new Error(`Failed to generate report. Query status: ${status}`);
+ }
+
+ logger.info(`Athena query ${queryExecutionId} finished`);
+
+ return this.storageRepository.publishReport(
+ queryExecutionId,
+ reportFilePath,
+ );
+ }
+
+ private async poll(
+ queryId: string,
+ maxPollLimit: number,
+ waitForInSeconds: number,
+ ) {
+ let count = 0;
+ let status = 'QUEUED';
+
+ while (
+ count < maxPollLimit &&
+ ['QUEUED', 'RUNNING', 'UNKNOWN'].includes(status)
+ ) {
+ status = (await this.dataRepository.getQueryStatus(queryId)) || 'UNKNOWN';
+
+ count += 1;
+
+ await sleep(waitForInSeconds);
+ }
+
+ return status;
+ }
+}
diff --git a/utils/utils/src/reporting/storage-repository.ts b/utils/utils/src/reporting/storage-repository.ts
new file mode 100644
index 00000000..5a9444e6
--- /dev/null
+++ b/utils/utils/src/reporting/storage-repository.ts
@@ -0,0 +1,39 @@
+import { CopyObjectCommand, S3Client } from '@aws-sdk/client-s3';
+import type { Logger } from '../logger';
+
+export type IStorageRepository = {
+ publishReport: (
+ reportQueryId: string,
+ reportFilePath: string,
+ ) => Promise;
+};
+
+type StorageRepositoryDependencies = {
+ s3Client: S3Client;
+ reportingBucketName: string;
+ logger: Logger;
+};
+
+export const createStorageRepository = ({
+ logger,
+ reportingBucketName,
+ s3Client,
+}: StorageRepositoryDependencies): IStorageRepository => ({
+ async publishReport(reportQueryId: string, reportFilePath: string) {
+ logger.debug(
+ `Publishing report data to ${reportFilePath} for query ${reportQueryId}`,
+ );
+
+ const copyObjectCommand = new CopyObjectCommand({
+ CopySource: `${reportingBucketName}/athena-output/${reportQueryId}.csv`,
+ Bucket: reportingBucketName,
+ Key: reportFilePath,
+ });
+
+ await s3Client.send(copyObjectCommand);
+
+ logger.info(`Report stored at ${reportingBucketName}/${reportFilePath}.`);
+
+ return `s3://${reportingBucketName}/${reportFilePath}`;
+ },
+});