diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md
index fd16d4e6..c05256f5 100644
--- a/infrastructure/terraform/components/api/README.md
+++ b/infrastructure/terraform/components/api/README.md
@@ -32,12 +32,13 @@ No requirements.
| Name | Source | Version |
|------|--------|---------|
-| [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip | n/a |
+| [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
| [domain\_truststore](#module\_domain\_truststore) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |
-| [get\_letters](#module\_get\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip | n/a |
+| [get\_letter\_data](#module\_get\_letter\_data) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
+| [get\_letters](#module\_get\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
| [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-kms.zip | n/a |
| [logging\_bucket](#module\_logging\_bucket) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |
-| [patch\_letter](#module\_patch\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip | n/a |
+| [patch\_letter](#module\_patch\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
| [s3bucket\_test\_letters](#module\_s3bucket\_test\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-s3bucket.zip | n/a |
| [supplier\_ssl](#module\_supplier\_ssl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-ssl.zip | n/a |
## Outputs
diff --git a/infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf b/infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf
index 4ceca3cd..7faefb31 100644
--- a/infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf
+++ b/infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf
@@ -50,7 +50,8 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
resources = [
module.authorizer_lambda.function_arn,
module.get_letters.function_arn,
- module.patch_letter.function_arn
+ module.patch_letter.function_arn,
+ module.get_letter_data.function_arn
]
}
}
diff --git a/infrastructure/terraform/components/api/locals.tf b/infrastructure/terraform/components/api/locals.tf
index 982bc110..c094c52f 100644
--- a/infrastructure/terraform/components/api/locals.tf
+++ b/infrastructure/terraform/components/api/locals.tf
@@ -9,6 +9,7 @@ locals {
AWS_REGION = var.region
AUTHORIZER_LAMBDA_ARN = module.authorizer_lambda.function_arn
GET_LETTERS_LAMBDA_ARN = module.get_letters.function_arn
+ GET_LETTER_DATA_LAMBDA_ARN = module.get_letter_data.function_arn
PATCH_LETTER_LAMBDA_ARN = module.patch_letter.function_arn
})
@@ -16,8 +17,9 @@ locals {
common_lambda_env_vars = {
LETTERS_TABLE_NAME = aws_dynamodb_table.letters.name,
- LETTER_TTL_HOURS = 24,
- SUPPLIER_ID_HEADER = "nhsd-supplier-id"
- APIM_CORRELATION_HEADER = "nhsd-correlation-id"
+ LETTER_TTL_HOURS = 12960, # 18 months * 30 days * 24 hours
+ SUPPLIER_ID_HEADER = "nhsd-supplier-id",
+ APIM_CORRELATION_HEADER = "nhsd-correlation-id",
+ DOWNLOAD_URL_TTL_SECONDS = 60
}
}
diff --git a/infrastructure/terraform/components/api/module_authorizer_lambda.tf b/infrastructure/terraform/components/api/module_authorizer_lambda.tf
index 19711a28..9d56c950 100644
--- a/infrastructure/terraform/components/api/module_authorizer_lambda.tf
+++ b/infrastructure/terraform/components/api/module_authorizer_lambda.tf
@@ -1,5 +1,5 @@
module "authorizer_lambda" {
- source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip"
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"
aws_account_id = var.aws_account_id
component = var.component
diff --git a/infrastructure/terraform/components/api/module_lambda_get_letter_data.tf b/infrastructure/terraform/components/api/module_lambda_get_letter_data.tf
new file mode 100644
index 00000000..5bf0597d
--- /dev/null
+++ b/infrastructure/terraform/components/api/module_lambda_get_letter_data.tf
@@ -0,0 +1,78 @@
+module "get_letter_data" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"
+
+ function_name = "get_letter_data"
+ description = "Get the letter data"
+
+ aws_account_id = var.aws_account_id
+ component = var.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.get_letter_data_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 = "api-handler/dist"
+ function_include_common = true
+ handler_function_name = "getLetterData"
+ runtime = "nodejs22.x"
+ memory = 128
+ timeout = 5
+ log_level = var.log_level
+
+ force_lambda_code_deploy = var.force_lambda_code_deploy
+ enable_lambda_insights = false
+
+ send_to_firehose = true
+ log_destination_arn = local.destination_arn
+ log_subscription_role_arn = local.acct.log_subscription_role_arn
+
+ lambda_env_vars = merge(local.common_lambda_env_vars, {})
+}
+
+data "aws_iam_policy_document" "get_letter_data_lambda" {
+ statement {
+ sid = "KMSPermissions"
+ effect = "Allow"
+
+ actions = [
+ "kms:Decrypt",
+ "kms:GenerateDataKey",
+ ]
+
+ resources = [
+ module.kms.key_arn, ## Requires shared kms module
+ ]
+ }
+
+ statement {
+ sid = "AllowDynamoDBAccess"
+ effect = "Allow"
+
+ actions = [
+ "dynamodb:GetItem",
+ "dynamodb:Query"
+ ]
+
+ resources = [
+ aws_dynamodb_table.letters.arn,
+ "${aws_dynamodb_table.letters.arn}/index/supplierStatus-index"
+ ]
+ }
+
+ statement {
+ sid = "S3GetObjectForPresign"
+ actions = [
+ "s3:GetObject",
+ "s3:ListBucket"] # allows 404 response instead of 403 if object missing
+ resources = ["${module.s3bucket_test_letters.arn}/*"]
+ }
+}
diff --git a/infrastructure/terraform/components/api/module_lambda_get_letters.tf b/infrastructure/terraform/components/api/module_lambda_get_letters.tf
index 2695a8f8..fa2369c7 100644
--- a/infrastructure/terraform/components/api/module_lambda_get_letters.tf
+++ b/infrastructure/terraform/components/api/module_lambda_get_letters.tf
@@ -1,5 +1,5 @@
module "get_letters" {
- source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip"
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"
function_name = "get_letters"
description = "Get paginated letter ids"
diff --git a/infrastructure/terraform/components/api/module_lambda_patch_letter.tf b/infrastructure/terraform/components/api/module_lambda_patch_letter.tf
index 568eff56..942335b5 100644
--- a/infrastructure/terraform/components/api/module_lambda_patch_letter.tf
+++ b/infrastructure/terraform/components/api/module_lambda_patch_letter.tf
@@ -1,5 +1,5 @@
module "patch_letter" {
- source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.20/terraform-lambda.zip"
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"
function_name = "patch_letter"
description = "Update the status of a letter"
diff --git a/infrastructure/terraform/components/api/resources/spec.tmpl.json b/infrastructure/terraform/components/api/resources/spec.tmpl.json
index ec58b67e..a2284fe9 100644
--- a/infrastructure/terraform/components/api/resources/spec.tmpl.json
+++ b/infrastructure/terraform/components/api/resources/spec.tmpl.json
@@ -105,6 +105,66 @@
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${PATCH_LETTER_LAMBDA_ARN}/invocations"
}
}
+ },
+ "/letters/{id}/data": {
+ "get": {
+ "operationId": "getDataId",
+ "responses": {
+ "303": {
+ "description": "See Other",
+ "headers": {
+ "Location": {
+ "description": "The signed S3 URL of the data file to download",
+ "example": "https://examples3bucket.com/filelocation",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Resource not found"
+ },
+ "429": {
+ "description": "Too many requests"
+ },
+ "500": {
+ "description": "Server error"
+ }
+ },
+ "security": [
+ {
+ "LambdaAuthorizer": []
+ }
+ ],
+ "summary": "Fetch a data file",
+ "x-amazon-apigateway-integration": {
+ "contentHandling": "CONVERT_TO_TEXT",
+ "credentials": "${APIG_EXECUTION_ROLE_ARN}",
+ "httpMethod": "POST",
+ "passthroughBehavior": "WHEN_NO_TEMPLATES",
+ "responses": {
+ ".*": {
+ "statusCode": "200"
+ }
+ },
+ "timeoutInMillis": 29000,
+ "type": "AWS_PROXY",
+ "uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${GET_LETTER_DATA_LAMBDA_ARN}/invocations"
+ }
+ },
+ "parameters": [
+ {
+ "description": "Unique identifier of this resource",
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "example": "24L5eYSWGzCHlGmzNxuqVusPxDg",
+ "type": "string"
+ }
+ }
+ ]
}
}
}
diff --git a/lambdas/api-handler/package.json b/lambdas/api-handler/package.json
index fd665012..84601bbd 100644
--- a/lambdas/api-handler/package.json
+++ b/lambdas/api-handler/package.json
@@ -3,6 +3,7 @@
"esbuild": "^0.24.0"
},
"devDependencies": {
+ "@aws-sdk/s3-request-presigner": "^3.901.0",
"@tsconfig/node22": "^22.0.2",
"@types/aws-lambda": "^8.10.148",
"@types/jest": "^29.5.14",
diff --git a/lambdas/api-handler/src/config/__tests__/deps.test.ts b/lambdas/api-handler/src/config/__tests__/deps.test.ts
new file mode 100644
index 00000000..2028db88
--- /dev/null
+++ b/lambdas/api-handler/src/config/__tests__/deps.test.ts
@@ -0,0 +1,68 @@
+
+import type { Deps } from '../deps';
+
+describe('createDependenciesContainer', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.resetModules();
+
+ // pino
+ jest.mock('pino', () => ({
+ __esModule: true,
+ default: jest.fn(() => ({
+ info: jest.fn(),
+ error: jest.fn(),
+ warn: jest.fn(),
+ debug: jest.fn(),
+ })),
+ }));
+
+ jest.mock('@aws-sdk/client-s3', () => ({
+ S3Client: jest.fn(),
+ }));
+
+ // Repo client
+ jest.mock('../../../../../internal/datastore', () => ({
+ LetterRepository: jest.fn(),
+ }));
+
+ // Env
+ jest.mock('../env', () => ({
+ envVars: {
+ LETTERS_TABLE_NAME: 'LettersTable',
+ LETTER_TTL_HOURS: 12960,
+ SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
+ APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
+ DOWNLOAD_URL_TTL_SECONDS: 60
+ },
+ }));
+ });
+
+ test('constructs deps and wires repository config correctly', async () => {
+ // get current mock instances
+ const { S3Client } = jest.requireMock('@aws-sdk/client-s3') as { S3Client: jest.Mock };
+ const pinoMock = jest.requireMock('pino') as { default: jest.Mock };
+ const { LetterRepository } = jest.requireMock('../../../../../internal/datastore') as { LetterRepository: jest.Mock };
+
+ const { createDependenciesContainer } = require('../deps');
+ const deps: Deps = createDependenciesContainer();
+
+ expect(S3Client).toHaveBeenCalledTimes(1);
+ expect(pinoMock.default).toHaveBeenCalledTimes(1);
+
+ expect(LetterRepository).toHaveBeenCalledTimes(1);
+ const repoCtorArgs = (LetterRepository as jest.Mock).mock.calls[0];
+ expect(repoCtorArgs[2]).toEqual({
+ lettersTableName: 'LettersTable',
+ ttlHours: 12960
+ });
+
+ expect(deps.env).toEqual({
+ LETTERS_TABLE_NAME: 'LettersTable',
+ LETTER_TTL_HOURS: 12960,
+ SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
+ APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
+ DOWNLOAD_URL_TTL_SECONDS: 60
+ });
+ });
+});
diff --git a/lambdas/api-handler/src/config/__tests__/env.test.ts b/lambdas/api-handler/src/config/__tests__/env.test.ts
new file mode 100644
index 00000000..c24205ab
--- /dev/null
+++ b/lambdas/api-handler/src/config/__tests__/env.test.ts
@@ -0,0 +1,63 @@
+import { ZodError } from 'zod';
+
+describe('lambdaEnv', () => {
+ const OLD_ENV = process.env;
+
+ beforeEach(() => {
+ jest.resetModules(); // Clears cached modules
+ process.env = { ...OLD_ENV }; // Clone original env
+ });
+
+ afterAll(() => {
+ process.env = OLD_ENV; // Restore
+ });
+
+ it('should load all environment variables successfully', () => {
+ process.env.SUPPLIER_ID_HEADER = 'nhsd-supplier-id';
+ process.env.APIM_CORRELATION_HEADER = 'nhsd-correlation-id';
+ process.env.LETTERS_TABLE_NAME = 'letters-table';
+ process.env.LETTER_TTL_HOURS = '12960';
+ process.env.DOWNLOAD_URL_TTL_SECONDS = '60';
+ process.env.MAX_LIMIT = '2500';
+
+ const { envVars } = require('../env');
+
+ expect(envVars).toEqual({
+ SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
+ APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
+ LETTERS_TABLE_NAME: 'letters-table',
+ LETTER_TTL_HOURS: 12960,
+ DOWNLOAD_URL_TTL_SECONDS: 60,
+ MAX_LIMIT: 2500,
+ });
+ });
+
+ it('should throw if a required env var is missing', () => {
+ process.env.SUPPLIER_ID_HEADER = 'nhsd-supplier-id';
+ process.env.APIM_CORRELATION_HEADER = 'nhsd-correlation-id';
+ process.env.LETTERS_TABLE_NAME = undefined; // simulate missing var
+ process.env.LETTER_TTL_HOURS = '12960';
+ process.env.DOWNLOAD_URL_TTL_SECONDS = '60';
+
+ expect(() => require('../env')).toThrow(ZodError);
+ });
+
+ it('should not throw if optional are not set', () => {
+ process.env.SUPPLIER_ID_HEADER = 'nhsd-supplier-id';
+ process.env.APIM_CORRELATION_HEADER = 'nhsd-correlation-id';
+ process.env.LETTERS_TABLE_NAME = 'letters-table';
+ process.env.LETTER_TTL_HOURS = '12960';
+ process.env.DOWNLOAD_URL_TTL_SECONDS = '60';
+
+ const { envVars } = require('../env');
+
+ expect(envVars).toEqual({
+ SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
+ APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
+ LETTERS_TABLE_NAME: 'letters-table',
+ LETTER_TTL_HOURS: 12960,
+ DOWNLOAD_URL_TTL_SECONDS: 60,
+ MAX_LIMIT: undefined
+ });
+ });
+});
diff --git a/lambdas/api-handler/src/config/deps.ts b/lambdas/api-handler/src/config/deps.ts
new file mode 100644
index 00000000..6f9c3e84
--- /dev/null
+++ b/lambdas/api-handler/src/config/deps.ts
@@ -0,0 +1,36 @@
+import { S3Client } from "@aws-sdk/client-s3";
+import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
+import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
+import pino from 'pino';
+import { LetterRepository } from '../../../../internal/datastore';
+import { envVars, EnvVars } from "../config/env";
+
+export type Deps = {
+ s3Client: S3Client;
+ letterRepo: LetterRepository;
+ logger: pino.Logger,
+ env: EnvVars
+};
+
+function createLetterRepository(log: pino.Logger, envVars: EnvVars): LetterRepository {
+ const ddbClient = new DynamoDBClient({});
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
+ const config = {
+ lettersTableName: envVars.LETTERS_TABLE_NAME,
+ ttlHours: envVars.LETTER_TTL_HOURS
+ };
+
+ return new LetterRepository(docClient, log, config);
+}
+
+export function createDependenciesContainer(): Deps {
+
+ const log = pino();
+
+ return {
+ s3Client: new S3Client(),
+ letterRepo: createLetterRepository(log, envVars),
+ logger: log,
+ env: envVars
+ };
+}
diff --git a/lambdas/api-handler/src/config/env.ts b/lambdas/api-handler/src/config/env.ts
new file mode 100644
index 00000000..5dfe4d27
--- /dev/null
+++ b/lambdas/api-handler/src/config/env.ts
@@ -0,0 +1,14 @@
+import {z} from 'zod';
+
+const EnvVarsSchema = z.object({
+ SUPPLIER_ID_HEADER: z.string(),
+ APIM_CORRELATION_HEADER: z.string(),
+ LETTERS_TABLE_NAME: z.string(),
+ LETTER_TTL_HOURS: z.coerce.number().int(),
+ DOWNLOAD_URL_TTL_SECONDS: z.coerce.number().int(),
+ MAX_LIMIT: z.coerce.number().int().optional()
+});
+
+export type EnvVars = z.infer;
+
+export const envVars = EnvVarsSchema.parse(process.env);
diff --git a/lambdas/api-handler/src/config/lambda-config.ts b/lambdas/api-handler/src/config/lambda-config.ts
deleted file mode 100644
index 7ff98a0f..00000000
--- a/lambdas/api-handler/src/config/lambda-config.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-interface LambdaConfig {
- SUPPLIER_ID_HEADER: string;
- APIM_CORRELATION_HEADER: string;
-}
-
-export const lambdaConfig: LambdaConfig = {
- SUPPLIER_ID_HEADER: getEnv("SUPPLIER_ID_HEADER")!,
- APIM_CORRELATION_HEADER: getEnv("APIM_CORRELATION_HEADER")!
-};
-
-function getEnv(name: string, required = true): string | undefined {
- const value = process.env[name];
- if (!value && required) {
- throw new Error(`Missing required env var: ${name}`);
- }
- return value;
-}
diff --git a/lambdas/api-handler/src/contracts/errors.ts b/lambdas/api-handler/src/contracts/errors.ts
index 9dc98f60..f3b3114e 100644
--- a/lambdas/api-handler/src/contracts/errors.ts
+++ b/lambdas/api-handler/src/contracts/errors.ts
@@ -27,7 +27,6 @@ export enum ApiErrorStatus {
export enum ApiErrorDetail {
NotFoundLetterId = 'No resource found with that ID',
- InvalidRequestMissingSupplierId = 'The supplier ID is missing from the request',
InvalidRequestMissingBody = 'The request is missing the body',
InvalidRequestMissingLetterIdPathParameter = 'The request is missing the letter id path parameter',
InvalidRequestLetterIdsMismatch = 'The letter ID in the request body does not match the letter ID path parameter',
diff --git a/lambdas/api-handler/src/handlers/__tests__/get-letter-data.test.ts b/lambdas/api-handler/src/handlers/__tests__/get-letter-data.test.ts
new file mode 100644
index 00000000..5b6509a8
--- /dev/null
+++ b/lambdas/api-handler/src/handlers/__tests__/get-letter-data.test.ts
@@ -0,0 +1,127 @@
+// mock error mapper
+jest.mock('../../mappers/error-mapper');
+import { mapErrorToResponse } from '../../mappers/error-mapper';
+const mockedMapErrorToResponse = jest.mocked(mapErrorToResponse);
+const expectedErrorResponse: APIGatewayProxyResult = {
+ statusCode: 400,
+ body: 'Error'
+};
+mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
+
+// mock letterService
+jest.mock('../../services/letter-operations');
+import * as letterService from '../../services/letter-operations';
+
+import type { APIGatewayProxyResult, Context } from 'aws-lambda';
+import { mockDeep } from 'jest-mock-extended';
+import { makeApiGwEvent } from './utils/test-utils';
+import { ValidationError } from '../../errors';
+import * as errors from '../../contracts/errors';
+import { createGetLetterDataHandler } from '../get-letter-data';
+import { S3Client } from '@aws-sdk/client-s3';
+import pino from 'pino';
+import { LetterRepository } from '../../../../../internal/datastore/src';
+import { EnvVars } from '../../config/env';
+import { Deps } from "../../config/deps";
+
+describe('API Lambda handler', () => {
+
+ const mockedDeps: jest.Mocked = {
+ s3Client: {} as unknown as S3Client,
+ letterRepo: {} as unknown as LetterRepository,
+ logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
+ env: {
+ SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
+ APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
+ LETTERS_TABLE_NAME: 'LETTERS_TABLE_NAME',
+ LETTER_TTL_HOURS: 12960,
+ DOWNLOAD_URL_TTL_SECONDS: 60
+ } as unknown as EnvVars
+ }
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns 303 Found with a pre signed url', async () => {
+
+ const mockedGetLetterDataUrlService = letterService.getLetterDataUrl as jest.Mock;
+ mockedGetLetterDataUrlService.mockResolvedValue('https://somePreSignedUrl.com');
+
+ const event = makeApiGwEvent({
+ path: '/letters/letter1/data',
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ },
+ pathParameters: {id: 'id1'}
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const getLetterDataHandler = createGetLetterDataHandler(mockedDeps);
+ const result = await getLetterDataHandler(event, context, callback);
+
+ expect(result).toEqual({
+ statusCode: 303,
+ headers: {
+ 'Location': 'https://somePreSignedUrl.com',
+ },
+ body: ''
+ });
+ });
+
+ it('returns error if headers are empty', async () => {
+ const event = makeApiGwEvent({ path: '/letters/letter1/data', headers: {},
+ pathParameters: {id: 'id1'}
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const getLetterDataHandler = createGetLetterDataHandler(mockedDeps);
+ const result = await getLetterDataHandler(event, context, callback);
+
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined, mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+
+ it('returns error if correlation id not provided in request', async () => {
+ const event = makeApiGwEvent({
+ path: '/letters/letter1/data',
+ queryStringParameters: { limit: '2000' },
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'x-request-id': 'requestId'
+ },
+ pathParameters: {id: 'id1'}
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const getLetterDataHandler = createGetLetterDataHandler(mockedDeps);
+ const result = await getLetterDataHandler(event, context, callback);
+
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined, mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+
+ it('returns error response when path parameter letterId is not found', async () => {
+ const event = makeApiGwEvent({
+ path: '/letters/',
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ },
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const getLetterDataHandler = createGetLetterDataHandler(mockedDeps);
+ const result = await getLetterDataHandler(event, context, callback);
+
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter), 'correlationId', mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+});
diff --git a/lambdas/api-handler/src/handlers/__tests__/get-letters.test.ts b/lambdas/api-handler/src/handlers/__tests__/get-letters.test.ts
index a1ef63a1..73b52896 100644
--- a/lambdas/api-handler/src/handlers/__tests__/get-letters.test.ts
+++ b/lambdas/api-handler/src/handlers/__tests__/get-letters.test.ts
@@ -1,14 +1,6 @@
-import { getLetters } from '../../index';
-import type { APIGatewayProxyResult, Context } from 'aws-lambda';
-import { mockDeep } from 'jest-mock-extended';
-import { makeApiGwEvent } from './utils/test-utils';
-import * as letterService from '../../services/letter-operations';
-import { mapErrorToResponse } from '../../mappers/error-mapper';
-import { getEnvars } from '../get-letters';
-import { ValidationError } from '../../errors';
-import * as errors from '../../contracts/errors';
-
+// mock error mapper
jest.mock('../../mappers/error-mapper');
+import { mapErrorToResponse } from '../../mappers/error-mapper';
const mockedMapErrorToResponse = jest.mocked(mapErrorToResponse);
const expectedErrorResponse: APIGatewayProxyResult = {
statusCode: 400,
@@ -16,32 +8,40 @@ const expectedErrorResponse: APIGatewayProxyResult = {
};
mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
+//mock letter service
jest.mock('../../services/letter-operations');
+import * as letterService from '../../services/letter-operations';
-jest.mock('../../config/lambda-config', () => ({
- lambdaConfig: {
- SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
- APIM_CORRELATION_HEADER: 'nhsd-correlation-id'
- }
-}));
+import type { APIGatewayProxyResult, Context } from 'aws-lambda';
+import { mockDeep } from 'jest-mock-extended';
+import { makeApiGwEvent } from './utils/test-utils';
+import { ValidationError } from '../../errors';
+import * as errors from '../../contracts/errors';
+import { S3Client } from '@aws-sdk/client-s3';
+import pino from 'pino';
+import { LetterRepository } from '../../../../../internal/datastore/src';
+import { createGetLettersHandler } from '../get-letters';
+import { Deps } from '../../config/deps';
+import { EnvVars } from '../../config/env';
describe('API Lambda handler', () => {
- const originalEnv = process.env;
+ const mockedDeps: jest.Mocked = {
+ s3Client: {} as unknown as S3Client,
+ letterRepo: {} as unknown as LetterRepository,
+ logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
+ env: {
+ SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
+ APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
+ LETTERS_TABLE_NAME: 'LETTERS_TABLE_NAME',
+ LETTER_TTL_HOURS: 12960,
+ DOWNLOAD_URL_TTL_SECONDS: 60,
+ MAX_LIMIT: 2500
+ } as unknown as EnvVars
+ }
beforeEach(() => {
jest.clearAllMocks();
- jest.resetModules();
- process.env = { ...originalEnv };
- process.env.MAX_LIMIT = '2500';
- });
-
- afterEach(() => {
- process.env = originalEnv;
- });
-
- it('uses process.env.MAX_LIMIT for max limit set', async () => {
- expect(getEnvars().maxLimit).toBe(2500);
});
it('returns 200 OK with basic paginated resources', async () => {
@@ -49,49 +49,59 @@ describe('API Lambda handler', () => {
const mockedGetLetters = letterService.getLettersForSupplier as jest.Mock;
mockedGetLetters.mockResolvedValue([
{
- id: "l1",
- specificationId: "s1",
+ id: 'l1',
+ specificationId: 's1',
groupId: 'g1',
- status: "PENDING"
+ status: 'PENDING'
},
{
- id: "l2",
- specificationId: "s1",
+ id: 'l2',
+ specificationId: 's1',
groupId: 'g1',
- status: "PENDING",
+ status: 'PENDING',
},
{
- id: "l3",
- specificationId: "s1",
+ id: 'l3',
+ specificationId: 's1',
groupId: 'g1',
- status: "PENDING",
+ status: 'PENDING',
reasonCode: 123, // shouldn't be returned if present
- reasonText: "Reason text" // shouldn't be returned if present
+ reasonText: 'Reason text' // shouldn't be returned if present
},
]);
- const event = makeApiGwEvent({path: '/letters',
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}});
+ const event = makeApiGwEvent({
+ path: '/letters',
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
+ });
const context = mockDeep();
const callback = jest.fn();
- const result = await getLetters(event, context, callback);
+
+ const getLettersHandler = createGetLettersHandler(mockedDeps);
+ const result = await getLettersHandler(event, context, callback);
+
+ expect(mockedGetLetters).toHaveBeenCalledWith('supplier1', 'PENDING', mockedDeps.env.MAX_LIMIT, mockedDeps.letterRepo);
const expected = {
data: [
{
- id: "l1",
- type: "Letter",
- attributes: { status: "PENDING", specificationId: "s1", groupId: 'g1' },
+ id: 'l1',
+ type: 'Letter',
+ attributes: { status: 'PENDING', specificationId: 's1', groupId: 'g1' },
},
{
- id: "l2",
- type: "Letter",
- attributes: { status: "PENDING", specificationId: "s1", groupId: 'g1' },
+ id: 'l2',
+ type: 'Letter',
+ attributes: { status: 'PENDING', specificationId: 's1', groupId: 'g1' },
},
{
- id: "l3",
- type: "Letter",
- attributes: { status: "PENDING", specificationId: "s1", groupId: 'g1' }
+ id: 'l3',
+ type: 'Letter',
+ attributes: { status: 'PENDING', specificationId: 's1', groupId: 'g1' }
}
],
};
@@ -102,102 +112,163 @@ describe('API Lambda handler', () => {
});
});
- it("returns 400 if the limit parameter is not a number", async () => {
+ it('returns error if the limit parameter is not a number', async () => {
+
const event = makeApiGwEvent({
- path: "/letters",
- queryStringParameters: { limit: "1%" },
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ path: '/letters',
+ queryStringParameters: { limit: '1%' },
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
-
const context = mockDeep();
const callback = jest.fn();
- const result = await getLetters(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotANumber), 'correlationId');
+ const getLettersHandler = createGetLettersHandler(mockedDeps);
+ const result = await getLettersHandler(event, context, callback);
+
+
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotANumber), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
- it("returns 400 if the limit parameter is negative", async () => {
+ it('returns error if the limit parameter is negative', async () => {
const event = makeApiGwEvent({
- path: "/letters",
- queryStringParameters: { limit: "-1" },
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ path: '/letters',
+ queryStringParameters: { limit: '-1' },
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
-
const context = mockDeep();
const callback = jest.fn();
- const result = await getLetters(event, context, callback);
+
+ const getLettersHandler = createGetLettersHandler(mockedDeps);
+ const result = await getLettersHandler(event, context, callback);
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(
- new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [getEnvars().maxLimit] }), 'correlationId');
+ new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [mockedDeps.env.MAX_LIMIT] }), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
- it("returns 400 if the limit parameter is zero", async () => {
+ it('returns error if the limit parameter is zero', async () => {
const event = makeApiGwEvent({
- path: "/letters",
- queryStringParameters: { limit: "0" },
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ path: '/letters',
+ queryStringParameters: { limit: '0' },
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await getLetters(event, context, callback);
+
+ const getLettersHandler = createGetLettersHandler(mockedDeps);
+ const result = await getLettersHandler(event, context, callback);
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(
- new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [getEnvars().maxLimit] }), 'correlationId');
+ new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [mockedDeps.env.MAX_LIMIT] }), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
- it("returns 400 if the limit parameter is higher than max limit", async () => {
+ it('returns error if the limit parameter is higher than max limit', async () => {
const event = makeApiGwEvent({
- path: "/letters",
- queryStringParameters: { limit: "2501" },
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ path: '/letters',
+ queryStringParameters: { limit: '2501' },
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await getLetters(event, context, callback);
+
+ const getLettersHandler = createGetLettersHandler(mockedDeps);
+ const result = await getLettersHandler(event, context, callback);
expect(mockedMapErrorToResponse).toHaveBeenCalledWith(
- new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [getEnvars().maxLimit] }), 'correlationId');
+ new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [mockedDeps.env.MAX_LIMIT] }), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
- it("returns 400 if unknown parameters are present", async () => {
+ it('returns error if unknown parameters are present', async () => {
const event = makeApiGwEvent({
- path: "/letters",
- queryStringParameters: { max: "2000" },
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ path: '/letters',
+ queryStringParameters: { max: '2000' },
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await getLetters(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitOnly), 'correlationId');
+ const getLettersHandler = createGetLettersHandler(mockedDeps);
+ const result = await getLettersHandler(event, context, callback);
+
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitOnly), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
- it('returns 400 for missing supplier ID (empty headers)', async () => {
- const event = makeApiGwEvent({ path: "/letters", headers: {} });
+ it('returns error if headers are empty', async () => {
+ const event = makeApiGwEvent({ path: '/letters', headers: {} });
const context = mockDeep();
const callback = jest.fn();
- const result = await getLetters(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined);
+ const getLettersHandler = createGetLettersHandler(mockedDeps);
+ const result = await getLettersHandler(event, context, callback);
+
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined, mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
- it("returns 500 if correlation id not provided in request", async () => {
+ it('returns error if correlation id not provided in request', async () => {
const event = makeApiGwEvent({
- path: "/letters",
- queryStringParameters: { limit: "2000" },
- headers: {'nhsd-supplier-id': 'supplier1'}
+ path: '/letters',
+ queryStringParameters: { limit: '2000' },
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await getLetters(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined);
+ const getLettersHandler = createGetLettersHandler(mockedDeps);
+ const result = await getLettersHandler(event, context, callback);
+
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined, mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+
+ it('returns error if max limit is not set', async () => {
+ const event = makeApiGwEvent({path: '/letters',
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const mockedDepsNoMaxLimit = {
+ ...mockedDeps,
+ env: { ...mockedDeps.env },
+ };
+ delete mockedDepsNoMaxLimit.env.MAX_LIMIT;
+
+ const getLettersHandler = createGetLettersHandler(mockedDepsNoMaxLimit);
+ const result = await getLettersHandler(event, context, callback);
+
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('MAX_LIMIT is required for getLetters'), 'correlationId', mockedDepsNoMaxLimit.logger);
expect(result).toEqual(expectedErrorResponse);
});
});
diff --git a/lambdas/api-handler/src/handlers/__tests__/patch-letter.test.ts b/lambdas/api-handler/src/handlers/__tests__/patch-letter.test.ts
index 83f122ec..8c96ce79 100644
--- a/lambdas/api-handler/src/handlers/__tests__/patch-letter.test.ts
+++ b/lambdas/api-handler/src/handlers/__tests__/patch-letter.test.ts
@@ -1,23 +1,11 @@
-import { patchLetter } from '../../index';
-import { APIGatewayProxyResult, Context } from 'aws-lambda';
-import { mockDeep } from 'jest-mock-extended';
-import { makeApiGwEvent } from './utils/test-utils';
+// mock service
+jest.mock('../../services/letter-operations');
import * as letterService from '../../services/letter-operations';
-import { PatchLetterRequest, PatchLetterResponse } from '../../contracts/letters';
-import { mapErrorToResponse } from '../../mappers/error-mapper';
-import { ValidationError } from '../../errors';
-import * as errors from '../../contracts/errors';
+const mockedPatchLetterStatus = jest.mocked(letterService.patchLetterStatus);
-jest.mock('../../services/letter-operations');
+// mock mapper
jest.mock('../../mappers/error-mapper');
-
-jest.mock('../../config/lambda-config', () => ({
- lambdaConfig: {
- SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
- APIM_CORRELATION_HEADER: 'nhsd-correlation-id'
- }
-}));
-
+import { mapErrorToResponse } from '../../mappers/error-mapper';
const mockedMapErrorToResponse = jest.mocked(mapErrorToResponse);
const expectedErrorResponse: APIGatewayProxyResult = {
statusCode: 400,
@@ -25,7 +13,18 @@ const expectedErrorResponse: APIGatewayProxyResult = {
};
mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
-const mockedPatchLetterStatus = jest.mocked(letterService.patchLetterStatus);
+import { APIGatewayProxyResult, Context } from 'aws-lambda';
+import { mockDeep } from 'jest-mock-extended';
+import { makeApiGwEvent } from './utils/test-utils';
+import { PatchLetterRequest, PatchLetterResponse } from '../../contracts/letters';
+import { ValidationError } from '../../errors';
+import * as errors from '../../contracts/errors';
+import { S3Client } from '@aws-sdk/client-s3';
+import pino from 'pino';
+import { LetterRepository } from '../../../../../internal/datastore/src';
+import { EnvVars } from '../../config/env';
+import { createPatchLetterHandler } from '../patch-letter';
+import { Deps } from "../../config/deps";
const updateLetterStatusRequest : PatchLetterRequest = {
data: {
@@ -41,18 +40,35 @@ const updateLetterStatusRequest : PatchLetterRequest = {
const requestBody = JSON.stringify(updateLetterStatusRequest, null, 2);
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
describe('patchLetter API Handler', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockedDeps: jest.Mocked = {
+ s3Client: {} as unknown as S3Client,
+ letterRepo: {} as unknown as LetterRepository,
+ logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
+ env: {
+ SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
+ APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
+ LETTERS_TABLE_NAME: 'LETTERS_TABLE_NAME',
+ LETTER_TTL_HOURS: 12960,
+ DOWNLOAD_URL_TTL_SECONDS: 60
+ } as unknown as EnvVars
+ }
+
it('returns 200 OK with updated resource', async () => {
const event = makeApiGwEvent({
path: '/letters/id1',
body: requestBody,
pathParameters: {id: 'id1'},
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
@@ -72,7 +88,8 @@ describe('patchLetter API Handler', () => {
};
mockedPatchLetterStatus.mockResolvedValue(updateLetterServiceResponse);
- const result = await patchLetter(event, context, callback);
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
expect(result).toEqual({
statusCode: 200,
@@ -84,14 +101,19 @@ describe('patchLetter API Handler', () => {
const event = makeApiGwEvent({
path: '/letters/id1',
pathParameters: {id: 'id1'},
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await patchLetter(event, context, callback);
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingBody), 'correlationId');
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingBody), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -99,13 +121,19 @@ describe('patchLetter API Handler', () => {
const event = makeApiGwEvent({
path: '/letters/',
body: requestBody,
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await patchLetter(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter), 'correlationId');
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
+
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -117,14 +145,19 @@ describe('patchLetter API Handler', () => {
path: '/letters/id1',
body: requestBody,
pathParameters: {id: 'id1'},
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await patchLetter(event, context, callback);
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(error, 'correlationId');
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(error, 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -133,14 +166,18 @@ describe('patchLetter API Handler', () => {
path: '/letters/id1',
body: requestBody,
pathParameters: {id: 'id1'},
- headers: {'nhsd-correlation-id': 'correlationId'}
+ headers: {
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await patchLetter(event, context, callback);
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingSupplierId), 'correlationId');
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The supplier ID is missing from the request'), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -149,14 +186,19 @@ describe('patchLetter API Handler', () => {
path: '/letters/id1',
body: "{test: 'test'}",
pathParameters: {id: 'id1'},
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await patchLetter(event, context, callback);
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId');
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -165,14 +207,19 @@ describe('patchLetter API Handler', () => {
path: '/letters/id1',
body: '{#invalidJSON',
pathParameters: {id: 'id1'},
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await patchLetter(event, context, callback);
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId');
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -181,7 +228,11 @@ describe('patchLetter API Handler', () => {
path: '/letters/id1',
body: 'somebody',
pathParameters: {id: 'id1'},
- headers: {'nhsd-supplier-id': 'supplier1', 'nhsd-correlation-id': 'correlationId'}
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
@@ -191,9 +242,10 @@ describe('patchLetter API Handler', () => {
throw error;
});
- const result = await patchLetter(event, context, callback);
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(error, 'correlationId');
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(error, 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
spy.mockRestore();
@@ -204,18 +256,22 @@ describe('patchLetter API Handler', () => {
path: '/letters/id1',
body: requestBody,
pathParameters: {id: 'id1'},
- headers: {'nhsd-supplier-id': 'supplier1'}
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'x-request-id': 'requestId'
+ }
});
const context = mockDeep();
const callback = jest.fn();
- const result = await patchLetter(event, context, callback);
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined);
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined, mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
- it('returns 400 for missing supplier ID (empty headers)', async () => {
+ it('returns error if headers are empty', async () => {
const event = makeApiGwEvent({
path: '/letters/id1',
body: requestBody,
@@ -225,9 +281,30 @@ describe('patchLetter API Handler', () => {
const context = mockDeep();
const callback = jest.fn();
- const result = await patchLetter(event, context, callback);
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
+
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined, mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+
+ it('returns error when request id is missing', async () => {
+ const event = makeApiGwEvent({
+ path: '/letters/id1',
+ body: requestBody,
+ pathParameters: {id: 'id1'},
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId'
+ }
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const patchLetterHandler = createPatchLetterHandler(mockedDeps);
+ const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined);
+ expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the x-request-id"), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
});
diff --git a/lambdas/api-handler/src/handlers/get-letter-data.ts b/lambdas/api-handler/src/handlers/get-letter-data.ts
new file mode 100644
index 00000000..547c8e17
--- /dev/null
+++ b/lambdas/api-handler/src/handlers/get-letter-data.ts
@@ -0,0 +1,36 @@
+import { APIGatewayProxyHandler } from "aws-lambda";
+import { assertNotEmpty, validateCommonHeaders } from "../utils/validation";
+import { ApiErrorDetail } from '../contracts/errors';
+import { mapErrorToResponse } from "../mappers/error-mapper";
+import { ValidationError } from "../errors";
+import { getLetterDataUrl } from "../services/letter-operations";
+import type { Deps } from "../config/deps";
+
+
+export function createGetLetterDataHandler(deps: Deps): APIGatewayProxyHandler {
+
+ return async (event) => {
+
+ const commonHeadersResult = validateCommonHeaders(event.headers, deps);
+
+ if (!commonHeadersResult.ok) {
+ return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
+ }
+
+ try {
+ const letterId = assertNotEmpty( event.pathParameters?.id,
+ new ValidationError(ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter));
+
+ return {
+ statusCode: 303,
+ headers: {
+ 'Location': await getLetterDataUrl(commonHeadersResult.value.supplierId, letterId, deps)
+ },
+ body: ''
+ };
+ }
+ catch (error) {
+ return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
+ }
+ }
+};
diff --git a/lambdas/api-handler/src/handlers/get-letters.ts b/lambdas/api-handler/src/handlers/get-letters.ts
index 1f6f7df9..0d42ea20 100644
--- a/lambdas/api-handler/src/handlers/get-letters.ts
+++ b/lambdas/api-handler/src/handlers/get-letters.ts
@@ -1,114 +1,119 @@
-import { APIGatewayProxyEventQueryStringParameters, APIGatewayProxyHandler } from "aws-lambda";
-import { getLettersForSupplier } from "../services/letter-operations";
-import { createLetterRepository } from "../infrastructure/letter-repo-factory";
-import { assertNotEmpty, lowerCaseKeys } from "../utils/validation";
+import { APIGatewayProxyEventQueryStringParameters, APIGatewayProxyHandler } from 'aws-lambda';
+import { getLettersForSupplier } from '../services/letter-operations';
+import { validateCommonHeaders } from '../utils/validation';
import { ApiErrorDetail } from '../contracts/errors';
-import { lambdaConfig } from "../config/lambda-config";
-import pino from 'pino';
-import { mapErrorToResponse } from "../mappers/error-mapper";
-import { ValidationError } from "../errors";
-import { mapToGetLettersResponse } from "../mappers/letter-mapper";
+import { mapErrorToResponse } from '../mappers/error-mapper';
+import { ValidationError } from '../errors';
+import { mapToGetLettersResponse } from '../mappers/letter-mapper';
+import type { Deps } from '../config/deps';
+import { Logger } from 'pino';
-const letterRepo = createLetterRepository();
-
-const log = pino();
-
-export const getEnvars = (): { maxLimit: number } => ({
- maxLimit: parseInt(process.env.MAX_LIMIT!)
-});
// The endpoint should only return pending letters for now
-const status = "PENDING";
+const status = 'PENDING';
-export const getLetters: APIGatewayProxyHandler = async (event) => {
+export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler {
- const { maxLimit } = getEnvars();
- let correlationId;
+ return async (event) => {
- try {
- assertNotEmpty(event.headers, new Error("The request headers are empty"));
- const lowerCasedHeaders = lowerCaseKeys(event.headers);
- correlationId = assertNotEmpty(lowerCasedHeaders[lambdaConfig.APIM_CORRELATION_HEADER], new Error("The request headers don't contain the APIM correlation id"));
- const supplierId = assertNotEmpty(lowerCasedHeaders[lambdaConfig.SUPPLIER_ID_HEADER], new ValidationError(ApiErrorDetail.InvalidRequestMissingSupplierId));
- const limitNumber = getLimitOrDefault(event.queryStringParameters, maxLimit);
+ const commonHeadersResult = validateCommonHeaders(event.headers, deps);
- const letters = await getLettersForSupplier(
- supplierId,
- status,
- limitNumber,
- letterRepo,
- );
+ if (!commonHeadersResult.ok) {
+ return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
+ }
- const response = mapToGetLettersResponse(letters);
+ try {
+ const maxLimit = getMaxLimit(deps);
- log.info({
- description: 'Pending letters successfully fetched',
- supplierId,
- limitNumber,
- status,
- lettersCount: letters.length
- });
+ const limitNumber = getLimitOrDefault(event.queryStringParameters, maxLimit, deps.logger);
- return {
- statusCode: 200,
- body: JSON.stringify(response, null, 2),
- };
- }
- catch (error) {
- return mapErrorToResponse(error, correlationId);
- }
-};
+ const letters = await getLettersForSupplier(
+ commonHeadersResult.value.supplierId,
+ status,
+ limitNumber,
+ deps.letterRepo,
+ );
-function getLimitOrDefault(queryStringParameters: APIGatewayProxyEventQueryStringParameters | null, maxLimit: number) : number {
+ const response = mapToGetLettersResponse(letters);
- validateLimitParamOnly(queryStringParameters);
- return getLimit(queryStringParameters?.limit, maxLimit);
-}
+ deps.logger.info({
+ description: 'Pending letters successfully fetched',
+ supplierId: commonHeadersResult.value.supplierId,
+ limitNumber,
+ status,
+ lettersCount: letters.length
+ });
-function assertIsNumber(limitNumber: number) {
- if (isNaN(limitNumber)) {
- log.info({
- description: "limit parameter is not a number",
- limitNumber,
- });
- throw new ValidationError(ApiErrorDetail.InvalidRequestLimitNotANumber);
+ return {
+ statusCode: 200,
+ body: JSON.stringify(response, null, 2),
+ };
+ }
+ catch (error) {
+ return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
+ }
}
-}
+};
-function assertLimitInRange(limitNumber: number, maxLimit: number) {
- if (limitNumber <= 0 || limitNumber > maxLimit) {
- log.info({
- description: "Limit value is invalid",
- limitNumber,
- });
- throw new ValidationError(ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [maxLimit]});
- }
+function getLimitOrDefault(queryStringParameters: APIGatewayProxyEventQueryStringParameters | null, maxLimit: number, logger: Logger) : number {
+
+ validateLimitParamOnly(queryStringParameters, logger);
+ return getLimit(queryStringParameters?.limit, maxLimit, logger);
}
-function validateLimitParamOnly(queryStringParameters: APIGatewayProxyEventQueryStringParameters | null) {
+function validateLimitParamOnly(queryStringParameters: APIGatewayProxyEventQueryStringParameters | null, logger: Logger) {
if (
queryStringParameters &&
Object.keys(queryStringParameters).some(
- (key) => key !== "limit"
+ (key) => key !== 'limit'
)
) {
- log.info({
- description: "Unexpected query parameter(s) present",
+ logger.info({
+ description: 'Unexpected query parameter(s) present',
queryStringParameters: queryStringParameters,
});
throw new ValidationError(ApiErrorDetail.InvalidRequestLimitOnly);
}
}
-function getLimit(limit: string | undefined, maxLimit: number) {
+function getLimit(limit: string | undefined, maxLimit: number, logger: Logger) {
let result;
if (limit) {
let limitParam = limit;
result = Number(limitParam);
- assertIsNumber(result);
- assertLimitInRange(result, maxLimit);
+ assertIsNumber(result, logger);
+ assertLimitInRange(result, maxLimit, logger);
} else {
result = maxLimit;
}
return result;
}
+
+function assertIsNumber(limitNumber: number, logger: Logger) {
+ if (isNaN(limitNumber)) {
+ logger.info({
+ description: 'limit parameter is not a number',
+ limitNumber,
+ });
+ throw new ValidationError(ApiErrorDetail.InvalidRequestLimitNotANumber);
+ }
+}
+
+function assertLimitInRange(limitNumber: number, maxLimit: number, logger: Logger) {
+ if (limitNumber <= 0 || limitNumber > maxLimit) {
+ logger.info({
+ description: 'Limit value is invalid',
+ limitNumber,
+ });
+ throw new ValidationError(ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [maxLimit]});
+ }
+}
+
+function getMaxLimit(deps: Deps): number{
+
+ if (deps.env.MAX_LIMIT == null) {
+ throw new Error('MAX_LIMIT is required for getLetters');
+ }
+
+ return deps.env.MAX_LIMIT;
+}
diff --git a/lambdas/api-handler/src/handlers/patch-letter.ts b/lambdas/api-handler/src/handlers/patch-letter.ts
index c381d5bf..e8442fae 100644
--- a/lambdas/api-handler/src/handlers/patch-letter.ts
+++ b/lambdas/api-handler/src/handlers/patch-letter.ts
@@ -1,46 +1,49 @@
import { APIGatewayProxyHandler } from 'aws-lambda';
-import { createLetterRepository } from '../infrastructure/letter-repo-factory';
import { patchLetterStatus } from '../services/letter-operations';
import { PatchLetterRequest, PatchLetterRequestSchema } from '../contracts/letters';
import { ApiErrorDetail } from '../contracts/errors';
import { ValidationError } from '../errors';
import { mapErrorToResponse } from '../mappers/error-mapper';
-import { lambdaConfig } from "../config/lambda-config";
-import { assertNotEmpty, lowerCaseKeys } from '../utils/validation';
+import { assertNotEmpty, validateCommonHeaders } from '../utils/validation';
import { mapToLetterDto } from '../mappers/letter-mapper';
+import type { Deps } from "../config/deps";
-const letterRepo = createLetterRepository();
-export const patchLetter: APIGatewayProxyHandler = async (event) => {
- let correlationId;
+export function createPatchLetterHandler(deps: Deps): APIGatewayProxyHandler {
- try {
- assertNotEmpty(event.headers, new Error('The request headers are empty'));
- const lowerCasedHeaders = lowerCaseKeys(event.headers);
- correlationId = assertNotEmpty(lowerCasedHeaders[lambdaConfig.APIM_CORRELATION_HEADER], new Error("The request headers don't contain the APIM correlation id"));
- const supplierId = assertNotEmpty(lowerCasedHeaders[lambdaConfig.SUPPLIER_ID_HEADER], new ValidationError(ApiErrorDetail.InvalidRequestMissingSupplierId));
- const letterId = assertNotEmpty( event.pathParameters?.id, new ValidationError(ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter));
- const body = assertNotEmpty(event.body, new ValidationError(ApiErrorDetail.InvalidRequestMissingBody));
+ return async (event) => {
- let patchLetterRequest: PatchLetterRequest;
+ const commonHeadersResult = validateCommonHeaders(event.headers, deps);
+
+ if (!commonHeadersResult.ok) {
+ return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
+ }
try {
- patchLetterRequest = PatchLetterRequestSchema.parse(JSON.parse(body));
- } catch (error) {
- if (error instanceof Error) {
- throw new ValidationError(ApiErrorDetail.InvalidRequestBody, { cause: error});
+ const letterId = assertNotEmpty( event.pathParameters?.id,
+ new ValidationError(ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter));
+ const body = assertNotEmpty(event.body, new ValidationError(ApiErrorDetail.InvalidRequestMissingBody));
+
+ let patchLetterRequest: PatchLetterRequest;
+
+ try {
+ patchLetterRequest = PatchLetterRequestSchema.parse(JSON.parse(body));
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new ValidationError(ApiErrorDetail.InvalidRequestBody, { cause: error});
+ }
+ else throw error;
}
- else throw error;
- }
- const result = await patchLetterStatus(mapToLetterDto(patchLetterRequest, supplierId), letterId, letterRepo);
+ const updatedLetter = await patchLetterStatus(mapToLetterDto(patchLetterRequest, commonHeadersResult.value.supplierId), letterId, deps.letterRepo);
- return {
- statusCode: 200,
- body: JSON.stringify(result, null, 2)
- };
+ return {
+ statusCode: 200,
+ body: JSON.stringify(updatedLetter, null, 2)
+ };
- } catch (error) {
- return mapErrorToResponse(error, correlationId);
- }
+ } catch (error) {
+ return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
+ }
+ };
};
diff --git a/lambdas/api-handler/src/index.ts b/lambdas/api-handler/src/index.ts
index 203102f7..49a14008 100644
--- a/lambdas/api-handler/src/index.ts
+++ b/lambdas/api-handler/src/index.ts
@@ -1,3 +1,10 @@
-// Export all handlers for ease of access
-export { getLetters } from './handlers/get-letters';
-export { patchLetter } from './handlers/patch-letter';
+import { createDependenciesContainer } from "./config/deps";
+import { createGetLetterDataHandler } from "./handlers/get-letter-data";
+import { createGetLettersHandler } from "./handlers/get-letters";
+import { createPatchLetterHandler } from "./handlers/patch-letter";
+
+const container = createDependenciesContainer();
+
+export const getLetterData = createGetLetterDataHandler(container);
+export const getLetters = createGetLettersHandler(container);
+export const patchLetter = createPatchLetterHandler(container);
diff --git a/lambdas/api-handler/src/infrastructure/letter-repo-factory.ts b/lambdas/api-handler/src/infrastructure/letter-repo-factory.ts
deleted file mode 100644
index 15c69e21..00000000
--- a/lambdas/api-handler/src/infrastructure/letter-repo-factory.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
-import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
-import pino from 'pino';
-import { LetterRepository } from '../../../../internal/datastore';
-
-const BASE_TEN = 10;
-
-export function createLetterRepository(): LetterRepository {
- const ddbClient = new DynamoDBClient({});
- const docClient = DynamoDBDocumentClient.from(ddbClient);
- const log = pino();
- const config = {
- lettersTableName: process.env.LETTERS_TABLE_NAME!,
- ttlHours: parseInt(process.env.LETTER_TTL_HOURS!, BASE_TEN),
- };
-
- return new LetterRepository(docClient, log, config);
-}
diff --git a/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts b/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts
index 35e5aba9..5110e32b 100644
--- a/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts
+++ b/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts
@@ -1,12 +1,13 @@
import { mapErrorToResponse } from "../error-mapper";
import { ValidationError, NotFoundError } from "../../errors";
import { ApiErrorDetail } from "../../contracts/errors";
+import { Logger } from 'pino';
describe("mapErrorToResponse", () => {
it("should map ValidationError to InvalidRequest response", () => {
const err = new ValidationError(ApiErrorDetail.InvalidRequestLetterIdsMismatch);
- const res = mapErrorToResponse(err, 'correlationId');
+ const res = mapErrorToResponse(err, 'correlationId', { info: jest.fn(), error: jest.fn() } as unknown as Logger);
expect(res.statusCode).toEqual(400);
expect(JSON.parse(res.body)).toEqual({
@@ -28,7 +29,7 @@ describe("mapErrorToResponse", () => {
it("should map NotFoundError to NotFound response", () => {
const err = new NotFoundError(ApiErrorDetail.NotFoundLetterId);
- const res = mapErrorToResponse(err, undefined);
+ const res = mapErrorToResponse(err, undefined, { info: jest.fn(), error: jest.fn() } as unknown as Logger);
expect(res.statusCode).toEqual(404);
expect(JSON.parse(res.body)).toEqual({
@@ -50,7 +51,7 @@ describe("mapErrorToResponse", () => {
it("should map generic Error to InternalServerError response", () => {
const err = new Error("Something broke");
- const res = mapErrorToResponse(err, 'correlationId');
+ const res = mapErrorToResponse(err, 'correlationId', { info: jest.fn(), error: jest.fn() } as unknown as Logger);
expect(res.statusCode).toEqual(500);
expect(JSON.parse(res.body)).toEqual({
@@ -72,7 +73,7 @@ describe("mapErrorToResponse", () => {
it("should map unexpected non-error to InternalServerError response", () => {
const err = 12345; // not an Error
- const res = mapErrorToResponse(err, 'correlationId');
+ const res = mapErrorToResponse(err, 'correlationId', { info: jest.fn(), error: jest.fn() } as unknown as Logger);
expect(res.statusCode).toEqual(500);
expect(JSON.parse(res.body)).toEqual({
diff --git a/lambdas/api-handler/src/mappers/error-mapper.ts b/lambdas/api-handler/src/mappers/error-mapper.ts
index 5a9f09d7..99493234 100644
--- a/lambdas/api-handler/src/mappers/error-mapper.ts
+++ b/lambdas/api-handler/src/mappers/error-mapper.ts
@@ -1,15 +1,14 @@
import { APIGatewayProxyResult } from "aws-lambda";
import { NotFoundError, ValidationError } from "../errors";
import { buildApiError, ApiErrorCode, ApiErrorDetail, ApiErrorTitle, ApiError, ApiErrorStatus } from "../contracts/errors";
-import pino from "pino";
import { v4 as uuid } from 'uuid';
+import { Logger } from "pino";
-const logger = pino();
export interface ErrorResponse {
errors: ApiError[];
}
-export function mapErrorToResponse(error: unknown, correlationId: string | undefined): APIGatewayProxyResult {
+export function mapErrorToResponse(error: unknown, correlationId: string | undefined, logger: Logger): APIGatewayProxyResult {
if (error instanceof ValidationError) {
logger.info({ err: error }, `Validation error correlationId=${correlationId}`);
return buildResponseFromErrorCode(ApiErrorCode.InvalidRequest, error.detail, correlationId);
diff --git a/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts b/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
index 7420c311..b8338e84 100644
--- a/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
+++ b/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
@@ -1,27 +1,28 @@
-import { Letter } from '../../../../../internal/datastore/src';
-import { LetterDto, LetterStatus } from '../../contracts/letters';
-import { getLettersForSupplier, patchLetterStatus } from '../letter-operations';
-
-
-function makeLetter(id: string, status: Letter['status']) : Letter {
+import { Letter, LetterRepository } from '../../../../../internal/datastore/src';
+import { Deps } from '../../config/deps';
+import { LetterDto } from '../../contracts/letters';
+import { getLetterDataUrl, getLettersForSupplier, patchLetterStatus } from '../letter-operations';
+import pino from 'pino';
+
+jest.mock('@aws-sdk/s3-request-presigner', () => ({
+ getSignedUrl: jest.fn(),
+}));
+import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
+
+jest.mock('@aws-sdk/client-s3', () => {
+ const originalModule = jest.requireActual('@aws-sdk/client-s3');
return {
- id,
- status,
- supplierId: 'supplier1',
- specificationId: 'spec123',
- groupId: 'group123',
- url: 'https://example.com/letter/abc123',
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- supplierStatus: `supplier1#${status}`,
- supplierStatusSk: Date.now().toString(),
- ttl: 123,
- reasonCode: 123,
- reasonText: "Reason text"
+ GetObjectCommand: jest.fn().mockImplementation((input) => ({ input })),
};
-}
+});
+import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
describe("getLetterIdsForSupplier", () => {
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
it("returns letter IDs from the repository", async () => {
const mockRepo = {
getLettersBySupplier: jest.fn().mockResolvedValue([
@@ -51,6 +52,10 @@ describe("getLetterIdsForSupplier", () => {
describe('patchLetterStatus function', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
const updatedLetterDto: LetterDto = {
id: 'letter1',
supplierId: 'supplier1',
@@ -104,3 +109,78 @@ describe('patchLetterStatus function', () => {
await expect(patchLetterStatus(updatedLetterDto, 'letter1', mockRepo as any)).rejects.toThrow("unexpected error");
});
});
+
+describe('getLetterDataUrl function', () => {
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mockedGetSignedUrl = getSignedUrl as jest.MockedFunction;
+ const MockedGetObjectCommand = GetObjectCommand as unknown as jest.Mock;
+
+ const updatedLetter = makeLetter("letter1", "REJECTED");
+
+ const s3Client = { send: jest.fn() } as unknown as S3Client;
+ const letterRepo = {
+ getLetterById: jest.fn().mockResolvedValue(updatedLetter)
+ } as unknown as LetterRepository;
+ const logger = jest.fn() as unknown as pino.Logger;;
+ const env = {
+ LETTERS_TABLE_NAME: 'LettersTable',
+ LETTER_TTL_HOURS: 12960,
+ SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
+ APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
+ DOWNLOAD_URL_TTL_SECONDS: 60
+ };
+ const deps: Deps = { s3Client, letterRepo, logger, env };
+
+ it('should return pre signed url successfully', async () => {
+
+ mockedGetSignedUrl.mockResolvedValue('http://somePreSignedUrl.com');
+
+ const result = await getLetterDataUrl('supplier1', 'letter1', deps);
+
+ const expectedCommandInput = {
+ Bucket: 'letterDataBucket',
+ Key: 'letter1.pdf'
+ };
+ expect(mockedGetSignedUrl).toHaveBeenCalledWith(s3Client, { input: expectedCommandInput}, { expiresIn: 60});
+ expect(result).toEqual('http://somePreSignedUrl.com');
+ });
+
+ it('should throw notFoundError when letter does not exist', async () => {
+ deps.letterRepo = {
+ getLetterById: jest.fn().mockRejectedValue(new Error('Letter with id l1 not found for supplier s1'))
+ } as unknown as LetterRepository;
+
+ await expect(getLetterDataUrl('supplier1', 'letter42', deps)).rejects.toThrow("No resource found with that ID");
+ });
+
+ it('should throw unexpected error', async () => {
+
+ deps.letterRepo = {
+ getLetterById: jest.fn().mockRejectedValue(new Error('unexpected error'))
+ } as unknown as LetterRepository;
+
+ await expect(getLetterDataUrl('supplier1', 'letter1', deps)).rejects.toThrow("unexpected error");
+ });
+});
+
+function makeLetter(id: string, status: Letter['status']) : Letter {
+ return {
+ id,
+ status,
+ supplierId: 'supplier1',
+ specificationId: 'spec123',
+ groupId: 'group123',
+ url: `s3://letterDataBucket/${id}.pdf`,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ supplierStatus: `supplier1#${status}`,
+ supplierStatusSk: Date.now().toString(),
+ ttl: 123,
+ reasonCode: 123,
+ reasonText: "Reason text"
+ };
+}
diff --git a/lambdas/api-handler/src/services/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts
index 4344ef95..f64b72f9 100644
--- a/lambdas/api-handler/src/services/letter-operations.ts
+++ b/lambdas/api-handler/src/services/letter-operations.ts
@@ -3,6 +3,9 @@ import { NotFoundError, ValidationError } from '../errors';
import { LetterDto, PatchLetterResponse } from '../contracts/letters';
import { mapToPatchLetterResponse } from '../mappers/letter-mapper';
import { ApiErrorDetail } from '../contracts/errors';
+import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
+import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
+import { Deps } from '../config/deps';
export const getLettersForSupplier = async (supplierId: string, status: string, limit: number, letterRepo: LetterRepository): Promise => {
@@ -29,3 +32,32 @@ export const patchLetterStatus = async (letterToUpdate: LetterDto, letterId: str
return mapToPatchLetterResponse(updatedLetter);
}
+
+export const getLetterDataUrl = async (supplierId: string, letterId: string, deps: Deps): Promise => {
+
+ let letter;
+
+ try {
+ letter = await deps.letterRepo.getLetterById(supplierId, letterId);
+ return await getDownloadUrl(letter.url, deps.s3Client, deps.env.DOWNLOAD_URL_TTL_SECONDS);
+ } catch (error) {
+ if (error instanceof Error && /^Letter with id \w+ not found for supplier \w+$/.test(error.message)) {
+ throw new NotFoundError(ApiErrorDetail.NotFoundLetterId);
+ }
+ throw error;
+ }
+}
+
+async function getDownloadUrl(s3Uri: string, s3Client: S3Client, expiry: number) {
+
+ const url = new URL(s3Uri); // works for s3:// URIs
+ const bucket = url.hostname;
+ const key = url.pathname.slice(1); // remove leading '/'
+
+ const command = new GetObjectCommand({
+ Bucket: bucket,
+ Key: key,
+ });
+
+ return await getSignedUrl(s3Client, command, { expiresIn: expiry });
+}
diff --git a/lambdas/api-handler/src/utils/validation.ts b/lambdas/api-handler/src/utils/validation.ts
index 4bb50960..318131c4 100644
--- a/lambdas/api-handler/src/utils/validation.ts
+++ b/lambdas/api-handler/src/utils/validation.ts
@@ -1,3 +1,8 @@
+import { APIGatewayProxyEventHeaders } from 'aws-lambda';
+import { ValidationError } from '../errors';
+import { ApiErrorDetail } from '../contracts/errors';
+import { Deps } from '../config/deps';
+
export function assertNotEmpty(
value: T | null | undefined,
error: Error
@@ -6,11 +11,11 @@ export function assertNotEmpty(
throw error;
}
- if (typeof value === "string" && value.trim() === "") {
+ if (typeof value === 'string' && value.trim() === '') {
throw error;
}
- if (typeof value === "object" && Object.keys(value).length === 0) {
+ if (typeof value === 'object' && Object.keys(value).length === 0) {
throw error;
}
@@ -20,3 +25,38 @@ export function assertNotEmpty(
export function lowerCaseKeys(obj: Record): Record {
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k.toLowerCase(), v]));
}
+
+export function validateCommonHeaders(headers: APIGatewayProxyEventHeaders, deps: Deps
+): { ok: true; value: {correlationId: string, supplierId: string } } | { ok: false; error: Error; correlationId?: string } {
+
+ if (!headers || Object.keys(headers).length === 0) {
+ return { ok: false, error: new Error('The request headers are empty') };
+ }
+
+ const lowerCasedHeaders = lowerCaseKeys(headers);
+
+ const correlationId = lowerCasedHeaders[deps.env.APIM_CORRELATION_HEADER];
+ if (!correlationId) {
+ return { ok: false, error: new Error("The request headers don't contain the APIM correlation id") };
+ }
+
+ const requestId = lowerCasedHeaders['x-request-id'];
+ if (!requestId) {
+ return {
+ ok: false,
+ error: new Error("The request headers don't contain the x-request-id"),
+ correlationId
+ };
+ }
+
+ const supplierId = lowerCasedHeaders[deps.env.SUPPLIER_ID_HEADER];
+ if (!supplierId) {
+ return {
+ ok: false,
+ error: new Error('The supplier ID is missing from the request'),
+ correlationId
+ };
+ }
+
+ return { ok: true, value: { correlationId, supplierId } };
+}
diff --git a/package-lock.json b/package-lock.json
index 4c9878f5..224a5f2a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -118,6 +118,7 @@
"esbuild": "^0.24.0"
},
"devDependencies": {
+ "@aws-sdk/s3-request-presigner": "^3.901.0",
"@tsconfig/node22": "^22.0.2",
"@types/aws-lambda": "^8.10.148",
"@types/jest": "^29.5.14",
@@ -1643,6 +1644,156 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@aws-sdk/s3-request-presigner": {
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.901.0.tgz",
+ "integrity": "sha512-G/0G5tL3beETs2zkI0YQuM2SkrAsYJSe2vN2XtouVSN5c9v6EiSvdSsHAqMhLebnSs2suUkq0JO9ZotbXkUfMQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/signature-v4-multi-region": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@aws-sdk/util-format-url": "3.901.0",
+ "@smithy/middleware-endpoint": "^4.3.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/core": {
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.901.0.tgz",
+ "integrity": "sha512-brKAc3y64tdhyuEf+OPIUln86bRTqkLgb9xkd6kUdIeA5+qmp/N6amItQz+RN4k4O3kqkCPYnAd3LonTKluobw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.901.0",
+ "@aws-sdk/xml-builder": "3.901.0",
+ "@smithy/core": "^3.14.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/signature-v4": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-base64": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/middleware-sdk-s3": {
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.901.0.tgz",
+ "integrity": "sha512-prgjVC3fDT2VIlmQPiw/cLee8r4frTam9GILRUVQyDdNtshNwV3MiaSCLzzQJjKJlLgnBLNUHJCSmvUVtg+3iA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@aws-sdk/util-arn-parser": "3.893.0",
+ "@smithy/core": "^3.14.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/signature-v4": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-config-provider": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-stream": "^4.4.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.901.0.tgz",
+ "integrity": "sha512-2IWxbll/pRucp1WQkHi2W5E2SVPGBvk4Is923H7gpNksbVFws18ItjMM8ZpGm44cJEoy1zR5gjhLFklatpuoOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-sdk-s3": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/signature-v4": "^5.3.0",
+ "@smithy/types": "^4.6.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/types": {
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.901.0.tgz",
+ "integrity": "sha512-FfEM25hLEs4LoXsLXQ/q6X6L4JmKkKkbVFpKD4mwfVHtRVQG6QxJiCPcrkcPISquiy6esbwK2eh64TWbiD60cg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.6.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/s3-request-presigner/node_modules/@aws-sdk/xml-builder": {
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.901.0.tgz",
+ "integrity": "sha512-pxFCkuAP7Q94wMTNPAwi6hEtNrp/BdFf+HOrIEeFQsk4EoOmpKY3I6S+u6A9Wg295J80Kh74LqDWM22ux3z6Aw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.6.0",
+ "fast-xml-parser": "5.2.5",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/s3-request-presigner/node_modules/fast-xml-parser": {
+ "version": "5.2.5",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
+ "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "strnum": "^2.1.0"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/@aws-sdk/s3-request-presigner/node_modules/strnum": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
+ "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@aws-sdk/signature-v4-multi-region": {
"version": "3.896.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.896.0.tgz",
@@ -1739,6 +1890,36 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@aws-sdk/util-format-url": {
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.901.0.tgz",
+ "integrity": "sha512-GGUnJKrh3OF1F3YRSWtwPLbN904Fcfxf03gujyq1rcrDRPEkzoZB+2BzNkB27SsU6lAlwNq+4aRlZRVUloPiag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/querystring-builder": "^4.2.0",
+ "@smithy/types": "^4.6.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-format-url/node_modules/@aws-sdk/types": {
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.901.0.tgz",
+ "integrity": "sha512-FfEM25hLEs4LoXsLXQ/q6X6L4JmKkKkbVFpKD4mwfVHtRVQG6QxJiCPcrkcPISquiy6esbwK2eh64TWbiD60cg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.6.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@aws-sdk/util-locate-window": {
"version": "3.804.0",
"license": "Apache-2.0",
@@ -4269,12 +4450,12 @@
}
},
"node_modules/@smithy/abort-controller": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz",
- "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.0.tgz",
+ "integrity": "sha512-PLUYa+SUKOEZtXFURBu/CNxlsxfaFGxSBPcStL13KpVeVWIfdezWyDqkz7iDLmwnxojXD0s5KzuB5HGHvt4Aeg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.5.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4323,20 +4504,20 @@
}
},
"node_modules/@smithy/core": {
- "version": "3.12.0",
- "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.12.0.tgz",
- "integrity": "sha512-zJeAgogZfbwlPGL93y4Z/XNeIN37YCreRUd6YMIRvaq+6RnBK8PPYYIQ85Is/GglPh3kNImD5riDCXbVSDpCiQ==",
+ "version": "3.14.0",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.14.0.tgz",
+ "integrity": "sha512-XJ4z5FxvY/t0Dibms/+gLJrI5niRoY0BCmE02fwmPcRYFPI4KI876xaE79YGWIKnEslMbuQPsIEsoU/DXa0DoA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/middleware-serde": "^4.1.1",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/types": "^4.5.0",
- "@smithy/util-base64": "^4.1.0",
- "@smithy/util-body-length-browser": "^4.1.0",
- "@smithy/util-middleware": "^4.1.1",
- "@smithy/util-stream": "^4.3.2",
- "@smithy/util-utf8": "^4.1.0",
- "@smithy/uuid": "^1.0.0",
+ "@smithy/middleware-serde": "^4.2.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-base64": "^4.2.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-stream": "^4.4.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "@smithy/uuid": "^1.1.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4430,15 +4611,15 @@
}
},
"node_modules/@smithy/fetch-http-handler": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz",
- "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.0.tgz",
+ "integrity": "sha512-BG3KSmsx9A//KyIfw+sqNmWFr1YBUr+TwpxFT7yPqAk0yyDh7oSNgzfNH7pS6OC099EGx2ltOULvumCFe8bcgw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/querystring-builder": "^4.1.1",
- "@smithy/types": "^4.5.0",
- "@smithy/util-base64": "^4.1.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/querystring-builder": "^4.2.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-base64": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4503,9 +4684,9 @@
}
},
"node_modules/@smithy/is-array-buffer": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.1.0.tgz",
- "integrity": "sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz",
+ "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -4543,18 +4724,18 @@
}
},
"node_modules/@smithy/middleware-endpoint": {
- "version": "4.2.4",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.4.tgz",
- "integrity": "sha512-FZ4hzupOmthm8Q8ujYrd0I+/MHwVMuSTdkDtIQE0xVuvJt9pLT6Q+b0p4/t+slDyrpcf+Wj7SN+ZqT5OryaaZg==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.0.tgz",
+ "integrity": "sha512-jFVjuQeV8TkxaRlcCNg0GFVgg98tscsmIrIwRFeC74TIUyLE3jmY9xgc1WXrPQYRjQNK3aRoaIk6fhFRGOIoGw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.12.0",
- "@smithy/middleware-serde": "^4.1.1",
- "@smithy/node-config-provider": "^4.2.2",
- "@smithy/shared-ini-file-loader": "^4.2.0",
- "@smithy/types": "^4.5.0",
- "@smithy/url-parser": "^4.1.1",
- "@smithy/util-middleware": "^4.1.1",
+ "@smithy/core": "^3.14.0",
+ "@smithy/middleware-serde": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/shared-ini-file-loader": "^4.3.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/url-parser": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4582,13 +4763,13 @@
}
},
"node_modules/@smithy/middleware-serde": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz",
- "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.0.tgz",
+ "integrity": "sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/types": "^4.5.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4596,12 +4777,12 @@
}
},
"node_modules/@smithy/middleware-stack": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.1.1.tgz",
- "integrity": "sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.0.tgz",
+ "integrity": "sha512-G5CJ//eqRd9OARrQu9MK1H8fNm2sMtqFh6j8/rPozhEL+Dokpvi1Og+aCixTuwDAGZUkJPk6hJT5jchbk/WCyg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.5.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4609,14 +4790,14 @@
}
},
"node_modules/@smithy/node-config-provider": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.2.tgz",
- "integrity": "sha512-SYGTKyPvyCfEzIN5rD8q/bYaOPZprYUPD2f5g9M7OjaYupWOoQFYJ5ho+0wvxIRf471i2SR4GoiZ2r94Jq9h6A==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.0.tgz",
+ "integrity": "sha512-5QgHNuWdT9j9GwMPPJCKxy2KDxZ3E5l4M3/5TatSZrqYVoEiqQrDfAq8I6KWZw7RZOHtVtCzEPdYz7rHZixwcA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/property-provider": "^4.1.1",
- "@smithy/shared-ini-file-loader": "^4.2.0",
- "@smithy/types": "^4.5.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/shared-ini-file-loader": "^4.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4624,15 +4805,15 @@
}
},
"node_modules/@smithy/node-http-handler": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.2.1.tgz",
- "integrity": "sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.3.0.tgz",
+ "integrity": "sha512-RHZ/uWCmSNZ8cneoWEVsVwMZBKy/8123hEpm57vgGXA3Irf/Ja4v9TVshHK2ML5/IqzAZn0WhINHOP9xl+Qy6Q==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/abort-controller": "^4.1.1",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/querystring-builder": "^4.1.1",
- "@smithy/types": "^4.5.0",
+ "@smithy/abort-controller": "^4.2.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/querystring-builder": "^4.2.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4640,12 +4821,12 @@
}
},
"node_modules/@smithy/property-provider": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.1.1.tgz",
- "integrity": "sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.0.tgz",
+ "integrity": "sha512-rV6wFre0BU6n/tx2Ztn5LdvEdNZ2FasQbPQmDOPfV9QQyDmsCkOAB0osQjotRCQg+nSKFmINhyda0D3AnjSBJw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.5.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4653,12 +4834,12 @@
}
},
"node_modules/@smithy/protocol-http": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.2.1.tgz",
- "integrity": "sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.0.tgz",
+ "integrity": "sha512-6POSYlmDnsLKb7r1D3SVm7RaYW6H1vcNcTWGWrF7s9+2noNYvUsm7E4tz5ZQ9HXPmKn6Hb67pBDRIjrT4w/d7Q==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.5.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4666,13 +4847,13 @@
}
},
"node_modules/@smithy/querystring-builder": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.1.1.tgz",
- "integrity": "sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.0.tgz",
+ "integrity": "sha512-Q4oFD0ZmI8yJkiPPeGUITZj++4HHYCW3pYBYfIobUCkYpI6mbkzmG1MAQQ3lJYYWj3iNqfzOenUZu+jqdPQ16A==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.5.0",
- "@smithy/util-uri-escape": "^4.1.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-uri-escape": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4680,12 +4861,12 @@
}
},
"node_modules/@smithy/querystring-parser": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.1.1.tgz",
- "integrity": "sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.0.tgz",
+ "integrity": "sha512-BjATSNNyvVbQxOOlKse0b0pSezTWGMvA87SvoFoFlkRsKXVsN3bEtjCxvsNXJXfnAzlWFPaT9DmhWy1vn0sNEA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.5.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4705,12 +4886,12 @@
}
},
"node_modules/@smithy/shared-ini-file-loader": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.2.0.tgz",
- "integrity": "sha512-OQTfmIEp2LLuWdxa8nEEPhZmiOREO6bcB6pjs0AySf4yiZhl6kMOfqmcwcY8BaBPX+0Tb+tG7/Ia/6mwpoZ7Pw==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.0.tgz",
+ "integrity": "sha512-VCUPPtNs+rKWlqqntX0CbVvWyjhmX30JCtzO+s5dlzzxrvSfRh5SY0yxnkirvc1c80vdKQttahL71a9EsdolSQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.5.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4718,18 +4899,18 @@
}
},
"node_modules/@smithy/signature-v4": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.2.1.tgz",
- "integrity": "sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.0.tgz",
+ "integrity": "sha512-MKNyhXEs99xAZaFhm88h+3/V+tCRDQ+PrDzRqL0xdDpq4gjxcMmf5rBA3YXgqZqMZ/XwemZEurCBQMfxZOWq/g==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/is-array-buffer": "^4.1.0",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/types": "^4.5.0",
- "@smithy/util-hex-encoding": "^4.1.0",
- "@smithy/util-middleware": "^4.1.1",
- "@smithy/util-uri-escape": "^4.1.0",
- "@smithy/util-utf8": "^4.1.0",
+ "@smithy/is-array-buffer": "^4.2.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-hex-encoding": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-uri-escape": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4737,17 +4918,17 @@
}
},
"node_modules/@smithy/smithy-client": {
- "version": "4.6.4",
- "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.4.tgz",
- "integrity": "sha512-qL7O3VDyfzCSN9r+sdbQXGhaHtrfSJL30En6Jboj0I3bobf2g1/T0eP2L4qxqrEW26gWhJ4THI4ElVVLjYyBHg==",
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.7.0.tgz",
+ "integrity": "sha512-3BDx/aCCPf+kkinYf5QQhdQ9UAGihgOVqI3QO5xQfSaIWvUE4KYLtiGRWsNe1SR7ijXC0QEPqofVp5Sb0zC8xQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.12.0",
- "@smithy/middleware-endpoint": "^4.2.4",
- "@smithy/middleware-stack": "^4.1.1",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/types": "^4.5.0",
- "@smithy/util-stream": "^4.3.2",
+ "@smithy/core": "^3.14.0",
+ "@smithy/middleware-endpoint": "^4.3.0",
+ "@smithy/middleware-stack": "^4.2.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-stream": "^4.4.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4755,9 +4936,9 @@
}
},
"node_modules/@smithy/types": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.5.0.tgz",
- "integrity": "sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==",
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.6.0.tgz",
+ "integrity": "sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -4767,13 +4948,13 @@
}
},
"node_modules/@smithy/url-parser": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.1.1.tgz",
- "integrity": "sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.0.tgz",
+ "integrity": "sha512-AlBmD6Idav2ugmoAL6UtR6ItS7jU5h5RNqLMZC7QrLCoITA9NzIN3nx9GWi8g4z1pfWh2r9r96SX/jHiNwPJ9A==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/querystring-parser": "^4.1.1",
- "@smithy/types": "^4.5.0",
+ "@smithy/querystring-parser": "^4.2.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4781,13 +4962,13 @@
}
},
"node_modules/@smithy/util-base64": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.1.0.tgz",
- "integrity": "sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.2.0.tgz",
+ "integrity": "sha512-+erInz8WDv5KPe7xCsJCp+1WCjSbah9gWcmUXc9NqmhyPx59tf7jqFz+za1tRG1Y5KM1Cy1rWCcGypylFp4mvA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/util-buffer-from": "^4.1.0",
- "@smithy/util-utf8": "^4.1.0",
+ "@smithy/util-buffer-from": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4795,9 +4976,9 @@
}
},
"node_modules/@smithy/util-body-length-browser": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.1.0.tgz",
- "integrity": "sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz",
+ "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -4819,12 +5000,12 @@
}
},
"node_modules/@smithy/util-buffer-from": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.1.0.tgz",
- "integrity": "sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz",
+ "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/is-array-buffer": "^4.1.0",
+ "@smithy/is-array-buffer": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4832,9 +5013,9 @@
}
},
"node_modules/@smithy/util-config-provider": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.1.0.tgz",
- "integrity": "sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz",
+ "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -4892,9 +5073,9 @@
}
},
"node_modules/@smithy/util-hex-encoding": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.1.0.tgz",
- "integrity": "sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz",
+ "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -4904,12 +5085,12 @@
}
},
"node_modules/@smithy/util-middleware": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz",
- "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.0.tgz",
+ "integrity": "sha512-u9OOfDa43MjagtJZ8AapJcmimP+K2Z7szXn8xbty4aza+7P1wjFmy2ewjSbhEiYQoW1unTlOAIV165weYAaowA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.5.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4931,18 +5112,18 @@
}
},
"node_modules/@smithy/util-stream": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.2.tgz",
- "integrity": "sha512-Ka+FA2UCC/Q1dEqUanCdpqwxOFdf5Dg2VXtPtB1qxLcSGh5C1HdzklIt18xL504Wiy9nNUKwDMRTVCbKGoK69g==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.4.0.tgz",
+ "integrity": "sha512-vtO7ktbixEcrVzMRmpQDnw/Ehr9UWjBvSJ9fyAbadKkC4w5Cm/4lMO8cHz8Ysb8uflvQUNRcuux/oNHKPXkffg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/fetch-http-handler": "^5.2.1",
- "@smithy/node-http-handler": "^4.2.1",
- "@smithy/types": "^4.5.0",
- "@smithy/util-base64": "^4.1.0",
- "@smithy/util-buffer-from": "^4.1.0",
- "@smithy/util-hex-encoding": "^4.1.0",
- "@smithy/util-utf8": "^4.1.0",
+ "@smithy/fetch-http-handler": "^5.3.0",
+ "@smithy/node-http-handler": "^4.3.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-base64": "^4.2.0",
+ "@smithy/util-buffer-from": "^4.2.0",
+ "@smithy/util-hex-encoding": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4950,9 +5131,9 @@
}
},
"node_modules/@smithy/util-uri-escape": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.1.0.tgz",
- "integrity": "sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz",
+ "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -4962,12 +5143,12 @@
}
},
"node_modules/@smithy/util-utf8": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.1.0.tgz",
- "integrity": "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz",
+ "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/util-buffer-from": "^4.1.0",
+ "@smithy/util-buffer-from": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -4989,9 +5170,9 @@
}
},
"node_modules/@smithy/uuid": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.0.0.tgz",
- "integrity": "sha512-OlA/yZHh0ekYFnbUkmYBDQPE6fGfdrvgz39ktp8Xf+FA6BfxLejPTMDOG0Nfk5/rDySAz1dRbFf24zaAFYVXlQ==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz",
+ "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"