diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md
index 1392cc338..2f28d7fbd 100644
--- a/infrastructure/terraform/components/api/README.md
+++ b/infrastructure/terraform/components/api/README.md
@@ -38,8 +38,11 @@ No requirements.
| [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 |
+| [letter\_status\_update](#module\_letter\_status\_update) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
+| [letter\_status\_updates\_queue](#module\_letter\_status\_updates\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.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.24/terraform-lambda.zip | n/a |
+| [post\_letters](#module\_post\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
| [post\_mi](#module\_post\_mi) | 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 |
diff --git a/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf b/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf
new file mode 100644
index 000000000..62d99edd9
--- /dev/null
+++ b/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf
@@ -0,0 +1,12 @@
+resource "aws_lambda_event_source_mapping" "status_updates_sqs_to_status_update_handler" {
+ event_source_arn = module.letter_status_updates_queue.sqs_queue_arn
+ function_name = module.letter_status_update.function_arn
+ batch_size = 10
+ maximum_batching_window_in_seconds = 1
+ scaling_config { maximum_concurrency = 10 }
+
+ depends_on = [
+ module.letter_status_updates_queue, # ensures queue exists
+ module.letter_status_update # ensures update handler exists
+ ]
+}
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 2e90b260e..d6ff0cb43 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
@@ -53,6 +53,7 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
module.get_letter_data.function_arn,
module.get_letters.function_arn,
module.patch_letter.function_arn,
+ module.post_letters.function_arn,
module.post_mi.function_arn
]
}
diff --git a/infrastructure/terraform/components/api/locals.tf b/infrastructure/terraform/components/api/locals.tf
index 513e8da1b..a6621a3d8 100644
--- a/infrastructure/terraform/components/api/locals.tf
+++ b/infrastructure/terraform/components/api/locals.tf
@@ -5,14 +5,15 @@ locals {
root_domain_nameservers = local.acct.route53_zone_nameservers["supplier-api"]
openapi_spec = templatefile("${path.module}/resources/spec.tmpl.json", {
- APIG_EXECUTION_ROLE_ARN = aws_iam_role.api_gateway_execution_role.arn
- AWS_REGION = var.region
- AUTHORIZER_LAMBDA_ARN = module.authorizer_lambda.function_arn
- GET_LETTER_LAMBDA_ARN = module.get_letter.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
- POST_MI_LAMBDA_ARN = module.post_mi.function_arn
+ APIG_EXECUTION_ROLE_ARN = aws_iam_role.api_gateway_execution_role.arn
+ AWS_REGION = var.region
+ AUTHORIZER_LAMBDA_ARN = module.authorizer_lambda.function_arn
+ GET_LETTER_LAMBDA_ARN = module.get_letter.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
+ POST_LETTERS_LAMBDA_ARN = module.post_letters.function_arn
+ POST_MI_LAMBDA_ARN = module.post_mi.function_arn
})
destination_arn = "arn:aws:logs:${var.region}:${var.shared_infra_account_id}:destination:nhs-main-obs-firehose-logs"
diff --git a/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf b/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf
new file mode 100644
index 000000000..92c9d611d
--- /dev/null
+++ b/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf
@@ -0,0 +1,86 @@
+module "letter_status_update" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"
+
+ function_name = "letter_status_update"
+ description = "Processes letter status updates"
+
+ 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.letter_status_update.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 = "letterStatusUpdate"
+ 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" "letter_status_update" {
+ 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",
+ "dynamodb:UpdateItem",
+ ]
+
+ resources = [
+ aws_dynamodb_table.letters.arn,
+ ]
+ }
+
+ statement {
+ sid = "AllowQueueAccess"
+ effect = "Allow"
+
+ actions = [
+ "sqs:ReceiveMessage",
+ "sqs:DeleteMessage",
+ "sqs:GetQueueAttributes",
+ "sqs:ChangeMessageVisibility"
+ ]
+
+ resources = [
+ module.letter_status_updates_queue.sqs_queue_arn
+ ]
+ }
+}
diff --git a/infrastructure/terraform/components/api/module_lambda_post_letters.tf b/infrastructure/terraform/components/api/module_lambda_post_letters.tf
new file mode 100644
index 000000000..fb127948c
--- /dev/null
+++ b/infrastructure/terraform/components/api/module_lambda_post_letters.tf
@@ -0,0 +1,72 @@
+module "post_letters" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"
+
+ function_name = "post_letters"
+ description = "Receives and accepts collection of letters to update"
+
+ 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.post_letters.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 = "postLetters"
+ 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, {
+ QUEUE_URL = module.letter_status_updates_queue.sqs_queue_url,
+ MAX_LIMIT = var.max_get_limit
+ })
+}
+
+data "aws_iam_policy_document" "post_letters" {
+ statement {
+ sid = "KMSPermissions"
+ effect = "Allow"
+
+ actions = [
+ "kms:Decrypt",
+ "kms:GenerateDataKey",
+ ]
+
+ resources = [
+ module.kms.key_arn, ## Requires shared kms module
+ ]
+ }
+
+ statement {
+ sid = "AllowQueueAccess"
+ effect = "Allow"
+
+ actions = [
+ "sqs:SendMessage",
+ "sqs:GetQueueAttributes",
+ ]
+
+ resources = [
+ module.letter_status_updates_queue.sqs_queue_arn
+ ]
+ }
+}
diff --git a/infrastructure/terraform/components/api/module_sqs_letter_status_updates.tf b/infrastructure/terraform/components/api/module_sqs_letter_status_updates.tf
new file mode 100644
index 000000000..a604faaf5
--- /dev/null
+++ b/infrastructure/terraform/components/api/module_sqs_letter_status_updates.tf
@@ -0,0 +1,16 @@
+# Queue to transport update letter status messages
+module "letter_status_updates_queue" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip"
+
+ name = "letter_status_updates_queue"
+
+ aws_account_id = var.aws_account_id
+ component = var.component
+ environment = var.environment
+ project = var.project
+ region = var.region
+
+ sqs_kms_key_arn = module.kms.key_arn
+
+ create_dlq = true
+}
diff --git a/infrastructure/terraform/components/api/resources/spec.tmpl.json b/infrastructure/terraform/components/api/resources/spec.tmpl.json
index 4514619b0..76ed2dd1f 100644
--- a/infrastructure/terraform/components/api/resources/spec.tmpl.json
+++ b/infrastructure/terraform/components/api/resources/spec.tmpl.json
@@ -51,6 +51,43 @@
"type": "AWS_PROXY",
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${GET_LETTERS_LAMBDA_ARN}/invocations"
}
+ },
+ "post": {
+ "description": "Update the status of a collection of letters.",
+ "operationId": "postLetters",
+ "requestBody": {
+ "required": true
+ },
+ "responses": {
+ "202": {
+ "description": "Acknowledges letters will be updated"
+ },
+ "400": {
+ "description": "Bad request, invalid input data"
+ },
+ "500": {
+ "description": "Server error"
+ }
+ },
+ "security": [
+ {
+ "LambdaAuthorizer": []
+ }
+ ],
+ "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/${POST_LETTERS_LAMBDA_ARN}/invocations"
+ }
}
},
"/letters/{id}": {
diff --git a/internal/datastore/src/types.md b/internal/datastore/src/types.md
index 832460726..cef0f1103 100644
--- a/internal/datastore/src/types.md
+++ b/internal/datastore/src/types.md
@@ -31,14 +31,16 @@ erDiagram
erDiagram
MI {
string id
- string supplierId
- string specificationId
- string groupId
string lineItem
+ string timestamp
number quantity
+ string specificationId
+ string groupId
number stockRemaining
+ string supplierId
string createdAt
string updatedAt
+ number ttl "min: -9007199254740991, max: 9007199254740991"
}
```
diff --git a/lambdas/api-handler/package.json b/lambdas/api-handler/package.json
index d973c9d53..6c68e475c 100644
--- a/lambdas/api-handler/package.json
+++ b/lambdas/api-handler/package.json
@@ -1,5 +1,6 @@
{
"dependencies": {
+ "@aws-sdk/client-sqs": "^3.925.0",
"@internal/datastore": "*",
"@internal/helpers": "*",
"esbuild": "^0.25.11",
diff --git a/lambdas/api-handler/src/config/__tests__/deps.test.ts b/lambdas/api-handler/src/config/__tests__/deps.test.ts
index 4381d9618..e56fe2c54 100644
--- a/lambdas/api-handler/src/config/__tests__/deps.test.ts
+++ b/lambdas/api-handler/src/config/__tests__/deps.test.ts
@@ -32,6 +32,10 @@ describe('createDependenciesContainer', () => {
S3Client: jest.fn(),
}));
+ jest.mock('@aws-sdk/client-sqs', () => ({
+ SQSClient: jest.fn(),
+ }));
+
// Repo client
jest.mock('@internal/datastore', () => ({
LetterRepository: jest.fn(),
@@ -45,6 +49,7 @@ describe('createDependenciesContainer', () => {
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 { SQSClient } = jest.requireMock('@aws-sdk/client-sqs') as { SQSClient: jest.Mock };
const pinoMock = jest.requireMock('pino') as { default: jest.Mock };
const { LetterRepository, MIRepository } = jest.requireMock('@internal/datastore') as {
LetterRepository: jest.Mock,
@@ -55,6 +60,9 @@ describe('createDependenciesContainer', () => {
const deps: Deps = createDependenciesContainer();
expect(S3Client).toHaveBeenCalledTimes(1);
+
+ expect(SQSClient).toHaveBeenCalledTimes(1);
+
expect(pinoMock.default).toHaveBeenCalledTimes(1);
expect(LetterRepository).toHaveBeenCalledTimes(1);
diff --git a/lambdas/api-handler/src/config/__tests__/env.test.ts b/lambdas/api-handler/src/config/__tests__/env.test.ts
index 2399c9418..acf82296d 100644
--- a/lambdas/api-handler/src/config/__tests__/env.test.ts
+++ b/lambdas/api-handler/src/config/__tests__/env.test.ts
@@ -21,6 +21,7 @@ describe('lambdaEnv', () => {
process.env.MI_TTL_HOURS = '2160';
process.env.DOWNLOAD_URL_TTL_SECONDS = '60';
process.env.MAX_LIMIT = '2500';
+ process.env.QUEUE_URL = 'url';
const { envVars } = require('../env');
@@ -33,6 +34,7 @@ describe('lambdaEnv', () => {
MI_TTL_HOURS: 2160,
DOWNLOAD_URL_TTL_SECONDS: 60,
MAX_LIMIT: 2500,
+ QUEUE_URL: 'url'
});
});
diff --git a/lambdas/api-handler/src/config/deps.ts b/lambdas/api-handler/src/config/deps.ts
index 1942bc6cd..58739aba8 100644
--- a/lambdas/api-handler/src/config/deps.ts
+++ b/lambdas/api-handler/src/config/deps.ts
@@ -1,12 +1,14 @@
import { S3Client } from "@aws-sdk/client-s3";
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
+import { SQSClient } from "@aws-sdk/client-sqs";
import pino from 'pino';
import { LetterRepository, MIRepository } from '../../../../internal/datastore';
import { envVars, EnvVars } from "../config/env";
export type Deps = {
s3Client: S3Client;
+ sqsClient: SQSClient;
letterRepo: LetterRepository;
miRepo: MIRepository;
logger: pino.Logger;
@@ -18,37 +20,35 @@ function createDocumentClient(): DynamoDBDocumentClient {
return DynamoDBDocumentClient.from(ddbClient);
}
-function createLetterRepository(documentClient: DynamoDBDocumentClient, log: pino.Logger, envVars: EnvVars): LetterRepository {
- const ddbClient = new DynamoDBClient({});
- const docClient = DynamoDBDocumentClient.from(ddbClient);
+function createLetterRepository(log: pino.Logger, envVars: EnvVars): LetterRepository {
+
const config = {
lettersTableName: envVars.LETTERS_TABLE_NAME,
lettersTtlHours: envVars.LETTER_TTL_HOURS
};
- return new LetterRepository(docClient, log, config);
+ return new LetterRepository(createDocumentClient(), log, config);
}
-function createMIRepository(documentClient: DynamoDBDocumentClient, log: pino.Logger, envVars: EnvVars): MIRepository {
- const ddbClient = new DynamoDBClient({});
- const docClient = DynamoDBDocumentClient.from(ddbClient);
+function createMIRepository(log: pino.Logger, envVars: EnvVars): MIRepository {
+
const config = {
miTableName: envVars.MI_TABLE_NAME,
miTtlHours: envVars.MI_TTL_HOURS
};
- return new MIRepository(docClient, log, config);
+ return new MIRepository(createDocumentClient(), log, config);
}
export function createDependenciesContainer(): Deps {
const log = pino();
- const documentClient = createDocumentClient();
return {
s3Client: new S3Client(),
- letterRepo: createLetterRepository(documentClient, log, envVars),
- miRepo: createMIRepository(documentClient, log, envVars),
+ sqsClient: new SQSClient(),
+ letterRepo: createLetterRepository(log, envVars),
+ miRepo: createMIRepository(log, envVars),
logger: log,
env: envVars
};
diff --git a/lambdas/api-handler/src/config/env.ts b/lambdas/api-handler/src/config/env.ts
index f77f455ea..016f54d59 100644
--- a/lambdas/api-handler/src/config/env.ts
+++ b/lambdas/api-handler/src/config/env.ts
@@ -8,7 +8,8 @@ const EnvVarsSchema = z.object({
LETTER_TTL_HOURS: z.coerce.number().int(),
MI_TTL_HOURS: z.coerce.number().int(),
DOWNLOAD_URL_TTL_SECONDS: z.coerce.number().int(),
- MAX_LIMIT: z.coerce.number().int().optional()
+ MAX_LIMIT: z.coerce.number().int().optional(),
+ QUEUE_URL: z.coerce.string().optional()
});
export type EnvVars = z.infer;
diff --git a/lambdas/api-handler/src/contracts/errors.ts b/lambdas/api-handler/src/contracts/errors.ts
index a508739b3..44e54eacd 100644
--- a/lambdas/api-handler/src/contracts/errors.ts
+++ b/lambdas/api-handler/src/contracts/errors.ts
@@ -20,9 +20,9 @@ export enum ApiErrorTitle {
}
export enum ApiErrorStatus {
- InternalServerError = "500",
- InvalidRequest = "400",
- NotFound = "404"
+ InternalServerError = '500',
+ InvalidRequest = '400',
+ NotFound = '404'
}
export enum ApiErrorDetail {
@@ -35,7 +35,9 @@ export enum ApiErrorDetail {
InvalidRequestLimitNotInRange = 'The limit parameter must be a positive number not greater than %s',
InvalidRequestLimitOnly = "Only 'limit' query parameter is supported",
InvalidRequestNoRequestId = 'The request does not contain a request id',
- InvalidRequestTimestamp = 'Timestamps should be UTC date/times in ISO8601 format, with a Z suffix'
+ InvalidRequestTimestamp = 'Timestamps should be UTC date/times in ISO8601 format, with a Z suffix',
+ InvalidRequestLettersToUpdate = 'The request exceeds the maximum of %s items allowed for update',
+ InvalidRequestDuplicateLetterId = 'The request cannot include multiple letter objects with the same id'
}
export function buildApiError(params: {
@@ -54,3 +56,7 @@ export function buildApiError(params: {
detail: params.detail,
};
}
+
+export interface ErrorResponse {
+ errors: ApiError[];
+}
diff --git a/lambdas/api-handler/src/contracts/letters.ts b/lambdas/api-handler/src/contracts/letters.ts
index 1cc33a37d..6cab8c70e 100644
--- a/lambdas/api-handler/src/contracts/letters.ts
+++ b/lambdas/api-handler/src/contracts/letters.ts
@@ -1,16 +1,6 @@
import { z } from 'zod';
import { makeCollectionSchema, makeDocumentSchema } from './json-api';
-export type LetterDto = {
- id: string,
- status: LetterStatus,
- supplierId: string,
- specificationId?: string,
- groupId?: string,
- reasonCode?: string,
- reasonText?: string
-};
-
export const LetterStatusSchema = z.enum([
'PENDING',
'ACCEPTED',
@@ -25,6 +15,18 @@ export const LetterStatusSchema = z.enum([
'DELIVERED'
]);
+export const LetterDtoSchema = z.object({
+ id: z.string(),
+ status: LetterStatusSchema,
+ supplierId: z.string(),
+ specificationId: z.string().optional(),
+ groupId: z.string().optional(),
+ reasonCode: z.string().optional(),
+ reasonText: z.string().optional(),
+}).strict();
+
+export type LetterDto = z.infer;
+
export const PatchLetterRequestResourceSchema = z.object({
id: z.string(),
type: z.literal('Letter'),
@@ -62,11 +64,15 @@ export const PatchLetterResponseResourceSchema = GetLetterResponseResourceSchema
export type LetterStatus = z.infer;
export const PatchLetterRequestSchema = makeDocumentSchema(PatchLetterRequestResourceSchema);
+export const PatchLetterResponseSchema = makeDocumentSchema(PatchLetterResponseResourceSchema);
+export const PostLettersRequestSchema = makeCollectionSchema(PatchLetterRequestResourceSchema);
export const GetLetterResponseSchema = makeDocumentSchema(GetLetterResponseResourceSchema);
export const GetLettersResponseSchema = makeCollectionSchema(GetLettersResponseResourceSchema);
-export const PatchLetterResponseSchema = makeDocumentSchema(PatchLetterResponseResourceSchema);
+
+export type PostLettersRequestResource = z.infer;
export type PatchLetterRequest = z.infer;
+export type PatchLetterResponse = z.infer;
+export type PostLettersRequest = z.infer;
export type GetLetterResponse = z.infer;
export type GetLettersResponse = z.infer;
-export type PatchLetterResponse = z.infer;
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
index 38c339edc..6a5cd7911 100644
--- a/lambdas/api-handler/src/handlers/__tests__/get-letter-data.test.ts
+++ b/lambdas/api-handler/src/handlers/__tests__/get-letter-data.test.ts
@@ -1,12 +1,12 @@
// mock error mapper
jest.mock('../../mappers/error-mapper');
-import { mapErrorToResponse } from '../../mappers/error-mapper';
-const mockedMapErrorToResponse = jest.mocked(mapErrorToResponse);
+import { processError } from '../../mappers/error-mapper';
+const mockedProcessError = jest.mocked(processError);
const expectedErrorResponse: APIGatewayProxyResult = {
statusCode: 400,
body: 'Error'
};
-mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
+mockedProcessError.mockReturnValue(expectedErrorResponse);
// mock letterService
jest.mock('../../services/letter-operations');
@@ -82,7 +82,7 @@ describe('API Lambda handler', () => {
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(mockedProcessError).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined, mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -102,7 +102,7 @@ describe('API Lambda handler', () => {
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(mockedProcessError).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined, mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -121,7 +121,7 @@ describe('API Lambda handler', () => {
const getLetterDataHandler = createGetLetterDataHandler(mockedDeps);
const result = await getLetterDataHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter), 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).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 581e3960a..caa660ffe 100644
--- a/lambdas/api-handler/src/handlers/__tests__/get-letters.test.ts
+++ b/lambdas/api-handler/src/handlers/__tests__/get-letters.test.ts
@@ -1,12 +1,12 @@
// mock error mapper
jest.mock('../../mappers/error-mapper');
-import { mapErrorToResponse } from '../../mappers/error-mapper';
-const mockedMapErrorToResponse = jest.mocked(mapErrorToResponse);
+import { processError } from '../../mappers/error-mapper';
+const mockedProcessError = jest.mocked(processError);
const expectedErrorResponse: APIGatewayProxyResult = {
statusCode: 400,
body: 'Error'
};
-mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
+mockedProcessError.mockReturnValue(expectedErrorResponse);
//mock letter service
jest.mock('../../services/letter-operations');
@@ -175,7 +175,7 @@ describe('API Lambda handler', () => {
const result = await getLettersHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotANumber), 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotANumber), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -195,7 +195,7 @@ describe('API Lambda handler', () => {
const getLettersHandler = createGetLettersHandler(mockedDeps);
const result = await getLettersHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(
+ expect(mockedProcessError).toHaveBeenCalledWith(
new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [mockedDeps.env.MAX_LIMIT] }), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -216,7 +216,7 @@ describe('API Lambda handler', () => {
const getLettersHandler = createGetLettersHandler(mockedDeps);
const result = await getLettersHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(
+ expect(mockedProcessError).toHaveBeenCalledWith(
new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [mockedDeps.env.MAX_LIMIT] }), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -237,7 +237,7 @@ describe('API Lambda handler', () => {
const getLettersHandler = createGetLettersHandler(mockedDeps);
const result = await getLettersHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(
+ expect(mockedProcessError).toHaveBeenCalledWith(
new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitNotInRange, { args: [mockedDeps.env.MAX_LIMIT] }), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -258,7 +258,7 @@ describe('API Lambda handler', () => {
const getLettersHandler = createGetLettersHandler(mockedDeps);
const result = await getLettersHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitOnly), 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLimitOnly), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -270,7 +270,7 @@ describe('API Lambda handler', () => {
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(mockedProcessError).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined, mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -289,7 +289,7 @@ describe('API Lambda handler', () => {
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(mockedProcessError).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined, mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -313,7 +313,7 @@ describe('API Lambda handler', () => {
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(mockedProcessError).toHaveBeenCalledWith(new Error('Missing required environment variable: MAX_LIMIT'), 'correlationId', mockedDepsNoMaxLimit.logger);
expect(result).toEqual(expectedErrorResponse);
});
});
diff --git a/lambdas/api-handler/src/handlers/__tests__/letter-status-update.test.ts b/lambdas/api-handler/src/handlers/__tests__/letter-status-update.test.ts
new file mode 100644
index 000000000..b2730182a
--- /dev/null
+++ b/lambdas/api-handler/src/handlers/__tests__/letter-status-update.test.ts
@@ -0,0 +1,131 @@
+import { Context, SQSEvent, SQSRecord } from 'aws-lambda';
+import { mockDeep } from 'jest-mock-extended';
+import { LetterDto } from '../../contracts/letters';
+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';
+import { createLetterStatusUpdateHandler } from '../letter-status-update';
+
+describe('createLetterStatusUpdateHandler', () => {
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('processes letters successfully', async () => {
+
+ const lettersToUpdate: LetterDto[] = [
+ {
+ id: 'id1',
+ status: 'REJECTED',
+ supplierId: 's1',
+ specificationId: 'spec1',
+ groupId: 'g1',
+ reasonCode: '123',
+ reasonText: 'Reason text'
+ },
+ {
+ id: 'id2',
+ supplierId: 's2',
+ status: 'ACCEPTED'
+ },
+ {
+ id: 'id3',
+ supplierId: 's3',
+ status: 'DELIVERED'
+ }
+ ];
+
+ const mockedDeps: jest.Mocked = {
+ s3Client: {} as unknown as S3Client,
+ letterRepo: { updateLetterStatus: jest.fn()
+ .mockResolvedValueOnce(lettersToUpdate[0])
+ .mockResolvedValueOnce(lettersToUpdate[1])
+ .mockResolvedValueOnce(lettersToUpdate[2]) } 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,
+ QUEUE_URL: 'SQS_URL'
+ } as unknown as EnvVars
+ } as Deps;
+
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const letterStatusUpdateHandler = createLetterStatusUpdateHandler(mockedDeps);
+ await letterStatusUpdateHandler(buildEvent(lettersToUpdate), context, callback);
+
+ expect(mockedDeps.letterRepo.updateLetterStatus).toHaveBeenNthCalledWith(1, lettersToUpdate[0]);
+ expect(mockedDeps.letterRepo.updateLetterStatus).toHaveBeenNthCalledWith(2, lettersToUpdate[1]);
+ expect(mockedDeps.letterRepo.updateLetterStatus).toHaveBeenNthCalledWith(3, lettersToUpdate[2]);
+ });
+
+ it('logs error if error thrown when updating', async () => {
+
+ const mockError = new Error('Update error');
+
+ const mockedDeps: jest.Mocked = {
+ s3Client: {} as unknown as S3Client,
+ letterRepo: { updateLetterStatus: jest.fn().mockRejectedValue(mockError) } 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,
+ QUEUE_URL: 'SQS_URL'
+ } as unknown as EnvVars
+ } as Deps;
+
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const letterToUpdate: LetterDto[] = [{
+ id: 'id1',
+ status: 'ACCEPTED',
+ supplierId: 's1'
+ }];
+
+ const letterStatusUpdateHandler = createLetterStatusUpdateHandler(mockedDeps);
+ await letterStatusUpdateHandler(buildEvent(letterToUpdate), context, callback);
+
+ expect(mockedDeps.letterRepo.updateLetterStatus).toHaveBeenCalledWith(letterToUpdate[0]);
+ expect(mockedDeps.logger.error).toHaveBeenCalledWith({
+ err: mockError,
+ messageId: 'mid-id1',
+ correlationId: 'correlationId-id1',
+ messageBody: '{\"id\":\"id1\",\"status\":\"ACCEPTED\",\"supplierId\":\"s1\"}',
+ }, 'Error processing letter status update');
+ });
+});
+
+function buildEvent(lettersToUpdate: LetterDto[]): SQSEvent {
+
+ const records: Partial[] = lettersToUpdate.map(letter => {
+ return {
+ messageId: `mid-${letter.id}`,
+ body: JSON.stringify(letter),
+ messageAttributes: {
+ CorrelationId: {
+ dataType: 'String',
+ stringValue: `correlationId-${letter.id}`,
+ }
+ }
+ };
+ });
+
+ const event: Partial = {
+ Records: records as SQSRecord[],
+ };
+
+ return event as SQSEvent;
+}
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 6ec921116..dc573c5eb 100644
--- a/lambdas/api-handler/src/handlers/__tests__/patch-letter.test.ts
+++ b/lambdas/api-handler/src/handlers/__tests__/patch-letter.test.ts
@@ -5,13 +5,13 @@ const mockedPatchLetterStatus = jest.mocked(letterService.patchLetterStatus);
// mock mapper
jest.mock('../../mappers/error-mapper');
-import { mapErrorToResponse } from '../../mappers/error-mapper';
-const mockedMapErrorToResponse = jest.mocked(mapErrorToResponse);
+import { processError } from '../../mappers/error-mapper';
+const mockedProcessError = jest.mocked(processError);
const expectedErrorResponse: APIGatewayProxyResult = {
statusCode: 400,
body: 'Error'
};
-mockedMapErrorToResponse.mockReturnValue(expectedErrorResponse);
+mockedProcessError.mockReturnValue(expectedErrorResponse);
import { APIGatewayProxyResult, Context } from 'aws-lambda';
import { mockDeep } from 'jest-mock-extended';
@@ -113,7 +113,7 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingBody), 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingBody), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -133,7 +133,7 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter), 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -157,7 +157,7 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(error, 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).toHaveBeenCalledWith(error, 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -177,7 +177,7 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error('The supplier ID is missing from the request'), 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).toHaveBeenCalledWith(new Error('The supplier ID is missing from the request'), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -198,7 +198,7 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -219,7 +219,7 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -245,7 +245,7 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(error, 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).toHaveBeenCalledWith(error, 'correlationId', mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
spy.mockRestore();
@@ -267,7 +267,7 @@ describe('patchLetter API Handler', () => {
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, mockedDeps.logger);
+ expect(mockedProcessError).toHaveBeenCalledWith(new Error("The request headers don't contain the APIM correlation id"), undefined, mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -284,7 +284,7 @@ describe('patchLetter API Handler', () => {
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(mockedProcessError).toHaveBeenCalledWith(new Error('The request headers are empty'), undefined, mockedDeps.logger);
expect(result).toEqual(expectedErrorResponse);
});
@@ -304,7 +304,7 @@ describe('patchLetter API Handler', () => {
const patchLetterHandler = createPatchLetterHandler(mockedDeps);
const result = await patchLetterHandler(event, context, callback);
- expect(mockedMapErrorToResponse).toHaveBeenCalledWith(new Error("The request headers don't contain the x-request-id"), 'correlationId', mockedDeps.logger);
+ expect(mockedProcessError).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/__tests__/post-letters.test.ts b/lambdas/api-handler/src/handlers/__tests__/post-letters.test.ts
new file mode 100644
index 000000000..bd4d19ea7
--- /dev/null
+++ b/lambdas/api-handler/src/handlers/__tests__/post-letters.test.ts
@@ -0,0 +1,296 @@
+// mock service
+jest.mock('../../services/letter-operations');
+import * as letterService from '../../services/letter-operations';
+const mockedBatchUpdateStatus = jest.mocked(letterService.enqueueLetterUpdateRequests);
+
+// mock mapper
+jest.mock('../../mappers/error-mapper');
+import { processError } from '../../mappers/error-mapper';
+const mockedProcessError = jest.mocked(processError);
+const expectedErrorResponse: APIGatewayProxyResult = {
+ statusCode: 400,
+ body: 'Error'
+};
+mockedProcessError.mockReturnValue(expectedErrorResponse);
+
+import { APIGatewayProxyResult, Context } from 'aws-lambda';
+import { mockDeep } from 'jest-mock-extended';
+import { makeApiGwEvent } from './utils/test-utils';
+import { PostLettersRequest } 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 { Deps } from "../../config/deps";
+import { createPostLettersHandler } from '../post-letters';
+
+const updateLettersRequest : PostLettersRequest = {
+ data: [
+ {
+ id: 'id1',
+ type: 'Letter',
+ attributes: {
+ status: 'REJECTED',
+ reasonCode: '123',
+ reasonText: 'Reason text',
+ }
+ },
+ {
+ id: 'id2',
+ type: 'Letter',
+ attributes: {
+ status: 'ACCEPTED'
+ }
+ },
+ {
+ id: 'id3',
+ type: 'Letter',
+ attributes: {
+ status: 'DELIVERED'
+ }
+ },
+ ]
+};
+
+const requestBody = JSON.stringify(updateLettersRequest, null, 2);
+
+describe('postLetters 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,
+ MAX_LIMIT: 2500,
+ QUEUE_URL: 'SQS_URL'
+ } as unknown as EnvVars
+ } as Deps;
+
+ it('returns 202 Accepted', async () => {
+ const event = makeApiGwEvent({
+ path: '/letters',
+ body: requestBody,
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ mockedBatchUpdateStatus.mockResolvedValue();
+
+ const postLettersHandler = createPostLettersHandler(mockedDeps);
+ const result = await postLettersHandler(event, context, callback);
+
+ expect(result).toEqual({
+ statusCode: 202,
+ body: ''
+ });
+ });
+
+ it('returns error when supplier id is missing', async () => {
+ const event = makeApiGwEvent({
+ path: '/letters',
+ body: requestBody,
+ headers: {
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const postLettersHandler = createPostLettersHandler(mockedDeps);
+ const result = await postLettersHandler(event, context, callback);
+
+ expect(mockedProcessError).toHaveBeenCalledWith(new Error('The supplier ID is missing from the request'), 'correlationId', mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+
+ it('returns error if correlation id not provided in request', async () => {
+ const event = makeApiGwEvent({
+ path: '/letters',
+ body: requestBody,
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'x-request-id': 'requestId'
+ }
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const postLettersHandler = createPostLettersHandler(mockedDeps);
+ const result = await postLettersHandler(event, context, callback);
+
+ expect(mockedProcessError).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 there is no body', 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 postLettersHandler = createPostLettersHandler(mockedDeps);
+ const result = await postLettersHandler(event, context, callback);
+
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestMissingBody), 'correlationId', mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+
+ it('returns error when request body does not have correct shape', async () => {
+ const event = makeApiGwEvent({
+ path: '/letters',
+ body: "{test: 'test'}",
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const postLettersHandler = createPostLettersHandler(mockedDeps);
+ const result = await postLettersHandler(event, context, callback);
+
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId', mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+
+ it('returns error when request contains more than 2500 letters to update', async () => {
+
+ const event = makeApiGwEvent({
+ path: '/letters',
+ body: JSON.stringify({
+ data : Array.from({ length: 2501 },
+ () => ({
+ id: 'id1',
+ type: 'Letter',
+ attributes: {
+ status: 'ACCEPTED'
+ }
+ })
+ )
+ }),
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const postLettersHandler = createPostLettersHandler(mockedDeps);
+ const result = await postLettersHandler(event, context, callback);
+
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestLettersToUpdate, { args: [mockedDeps.env.MAX_LIMIT]}), 'correlationId', mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+
+ it('returns error when the request has duplicate letter ids', async () => {
+
+ const event = makeApiGwEvent({
+ path: '/letters',
+ body: JSON.stringify({
+ data: [
+ {
+ id: 'id1',
+ type: 'Letter',
+ attributes: {
+ status: 'ACCEPTED'
+ }
+ },
+ {
+ id: 'id1',
+ type: 'Letter',
+ attributes: {
+ status: 'ACCEPTED'
+ }
+ }
+ ]
+ }),
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const postLettersHandler = createPostLettersHandler(mockedDeps);
+ const result = await postLettersHandler(event, context, callback);
+
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestDuplicateLetterId), 'correlationId', mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+
+ it('returns error when request body is not json', async () => {
+ const event = makeApiGwEvent({
+ path: '/letters',
+ body: '{#invalidJSON',
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const postLettersHandler = createPostLettersHandler(mockedDeps);
+ const result = await postLettersHandler(event, context, callback);
+
+ expect(mockedProcessError).toHaveBeenCalledWith(new ValidationError(errors.ApiErrorDetail.InvalidRequestBody), 'correlationId', mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+ });
+
+ it('returns error if unexpected error is thrown', async () => {
+ const event = makeApiGwEvent({
+ path: '/letters',
+ body: 'somebody',
+ headers: {
+ 'nhsd-supplier-id': 'supplier1',
+ 'nhsd-correlation-id': 'correlationId',
+ 'x-request-id': 'requestId'
+ }
+ });
+ const context = mockDeep();
+ const callback = jest.fn();
+
+ const error = 'Unexpected error';
+ const spy = jest.spyOn(JSON, 'parse').mockImplementation(() => {
+ throw error;
+ });
+
+ const postLettersHandler = createPostLettersHandler(mockedDeps);
+ const result = await postLettersHandler(event, context, callback);
+
+ expect(mockedProcessError).toHaveBeenCalledWith(error, 'correlationId', mockedDeps.logger);
+ expect(result).toEqual(expectedErrorResponse);
+
+ spy.mockRestore();
+ });
+});
diff --git a/lambdas/api-handler/src/handlers/get-letter-data.ts b/lambdas/api-handler/src/handlers/get-letter-data.ts
index 547c8e170..59c880839 100644
--- a/lambdas/api-handler/src/handlers/get-letter-data.ts
+++ b/lambdas/api-handler/src/handlers/get-letter-data.ts
@@ -1,7 +1,7 @@
import { APIGatewayProxyHandler } from "aws-lambda";
import { assertNotEmpty, validateCommonHeaders } from "../utils/validation";
import { ApiErrorDetail } from '../contracts/errors';
-import { mapErrorToResponse } from "../mappers/error-mapper";
+import { processError } from "../mappers/error-mapper";
import { ValidationError } from "../errors";
import { getLetterDataUrl } from "../services/letter-operations";
import type { Deps } from "../config/deps";
@@ -14,7 +14,7 @@ export function createGetLetterDataHandler(deps: Deps): APIGatewayProxyHandler {
const commonHeadersResult = validateCommonHeaders(event.headers, deps);
if (!commonHeadersResult.ok) {
- return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
+ return processError(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
}
try {
@@ -30,7 +30,7 @@ export function createGetLetterDataHandler(deps: Deps): APIGatewayProxyHandler {
};
}
catch (error) {
- return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
+ return processError(error, commonHeadersResult.value.correlationId, deps.logger);
}
}
};
diff --git a/lambdas/api-handler/src/handlers/get-letter.ts b/lambdas/api-handler/src/handlers/get-letter.ts
index 5c428dcef..0cbe3b7a5 100644
--- a/lambdas/api-handler/src/handlers/get-letter.ts
+++ b/lambdas/api-handler/src/handlers/get-letter.ts
@@ -3,7 +3,7 @@ import { assertNotEmpty, validateCommonHeaders } from "../utils/validation";
import { ValidationError } from "../errors";
import { ApiErrorDetail } from "../contracts/errors";
import { getLetterById } from "../services/letter-operations";
-import { mapErrorToResponse } from "../mappers/error-mapper";
+import { processError } from "../mappers/error-mapper";
import { mapToGetLetterResponse } from "../mappers/letter-mapper";
import { Deps } from "../config/deps";
@@ -15,7 +15,7 @@ export function createGetLetterHandler(deps: Deps): APIGatewayProxyHandler {
const commonHeadersResult = validateCommonHeaders(event.headers, deps);
if (!commonHeadersResult.ok) {
- return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
+ return processError(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
}
try {
@@ -37,7 +37,7 @@ export function createGetLetterHandler(deps: Deps): APIGatewayProxyHandler {
};
} catch (error)
{
- return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
+ return processError(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 0d42ea206..ca258def9 100644
--- a/lambdas/api-handler/src/handlers/get-letters.ts
+++ b/lambdas/api-handler/src/handlers/get-letters.ts
@@ -1,14 +1,13 @@
import { APIGatewayProxyEventQueryStringParameters, APIGatewayProxyHandler } from 'aws-lambda';
import { getLettersForSupplier } from '../services/letter-operations';
-import { validateCommonHeaders } from '../utils/validation';
+import { requireEnvVar, validateCommonHeaders } from '../utils/validation';
import { ApiErrorDetail } from '../contracts/errors';
-import { mapErrorToResponse } from '../mappers/error-mapper';
+import { processError } from '../mappers/error-mapper';
import { ValidationError } from '../errors';
import { mapToGetLettersResponse } from '../mappers/letter-mapper';
import type { Deps } from '../config/deps';
import { Logger } from 'pino';
-
// The endpoint should only return pending letters for now
const status = 'PENDING';
@@ -19,11 +18,11 @@ export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler {
const commonHeadersResult = validateCommonHeaders(event.headers, deps);
if (!commonHeadersResult.ok) {
- return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
+ return processError(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
}
try {
- const maxLimit = getMaxLimit(deps);
+ const maxLimit = requireEnvVar(deps.env, "MAX_LIMIT");
const limitNumber = getLimitOrDefault(event.queryStringParameters, maxLimit, deps.logger);
@@ -50,7 +49,7 @@ export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler {
};
}
catch (error) {
- return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
+ return processError(error, commonHeadersResult.value.correlationId, deps.logger);
}
}
};
@@ -108,12 +107,3 @@ function assertLimitInRange(limitNumber: number, maxLimit: number, logger: Logge
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/letter-status-update.ts b/lambdas/api-handler/src/handlers/letter-status-update.ts
new file mode 100644
index 000000000..6e8e92414
--- /dev/null
+++ b/lambdas/api-handler/src/handlers/letter-status-update.ts
@@ -0,0 +1,24 @@
+import { SQSEvent, SQSHandler, SQSRecord } from 'aws-lambda';
+import { LetterDto, LetterDtoSchema } from '../contracts/letters';
+import { Deps } from '../config/deps';
+
+export function createLetterStatusUpdateHandler(deps: Deps): SQSHandler {
+
+ return async ( event: SQSEvent ) => {
+ const tasks = event.Records.map( async (message) => {
+ try {
+ const letterToUpdate: LetterDto = LetterDtoSchema.parse(JSON.parse(message.body));
+ await deps.letterRepo.updateLetterStatus(letterToUpdate);
+ } catch (error) {
+ deps.logger.error({
+ err: error,
+ messageId: message.messageId,
+ correlationId: message.messageAttributes['CorrelationId'].stringValue,
+ messageBody: message.body
+ }, 'Error processing letter status update');
+ }
+ });
+
+ await Promise.all(tasks);
+ }
+}
diff --git a/lambdas/api-handler/src/handlers/patch-letter.ts b/lambdas/api-handler/src/handlers/patch-letter.ts
index e8442fae9..42b5161c8 100644
--- a/lambdas/api-handler/src/handlers/patch-letter.ts
+++ b/lambdas/api-handler/src/handlers/patch-letter.ts
@@ -3,9 +3,9 @@ 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 { processError } from '../mappers/error-mapper';
import { assertNotEmpty, validateCommonHeaders } from '../utils/validation';
-import { mapToLetterDto } from '../mappers/letter-mapper';
+import { mapPatchLetterToDto } from '../mappers/letter-mapper';
import type { Deps } from "../config/deps";
@@ -16,7 +16,7 @@ export function createPatchLetterHandler(deps: Deps): APIGatewayProxyHandler {
const commonHeadersResult = validateCommonHeaders(event.headers, deps);
if (!commonHeadersResult.ok) {
- return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
+ return processError(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
}
try {
@@ -35,7 +35,7 @@ export function createPatchLetterHandler(deps: Deps): APIGatewayProxyHandler {
else throw error;
}
- const updatedLetter = await patchLetterStatus(mapToLetterDto(patchLetterRequest, commonHeadersResult.value.supplierId), letterId, deps.letterRepo);
+ const updatedLetter = await patchLetterStatus(mapPatchLetterToDto(patchLetterRequest, commonHeadersResult.value.supplierId), letterId, deps.letterRepo);
return {
statusCode: 200,
@@ -43,7 +43,7 @@ export function createPatchLetterHandler(deps: Deps): APIGatewayProxyHandler {
};
} catch (error) {
- return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
+ return processError(error, commonHeadersResult.value.correlationId, deps.logger);
}
};
};
diff --git a/lambdas/api-handler/src/handlers/post-letters.ts b/lambdas/api-handler/src/handlers/post-letters.ts
new file mode 100644
index 000000000..8437e9385
--- /dev/null
+++ b/lambdas/api-handler/src/handlers/post-letters.ts
@@ -0,0 +1,61 @@
+import { APIGatewayProxyHandler } from 'aws-lambda';
+import { enqueueLetterUpdateRequests } from '../services/letter-operations';
+import { PostLettersRequest, PostLettersRequestSchema } from '../contracts/letters';
+import { ApiErrorDetail } from '../contracts/errors';
+import { ValidationError } from '../errors';
+import { processError } from '../mappers/error-mapper';
+import { assertNotEmpty, requireEnvVar, validateCommonHeaders } from '../utils/validation';
+import type { Deps } from "../config/deps";
+
+export function createPostLettersHandler(deps: Deps): APIGatewayProxyHandler {
+
+ return async (event) => {
+
+ const commonHeadersResult = validateCommonHeaders(event.headers, deps);
+
+ if (!commonHeadersResult.ok) {
+ return processError(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
+ }
+
+ const maxUpdateItems = requireEnvVar(deps.env, "MAX_LIMIT");
+ requireEnvVar(deps.env, "QUEUE_URL");
+
+ try {
+ const body = assertNotEmpty(event.body, new ValidationError(ApiErrorDetail.InvalidRequestMissingBody));
+
+ let postLettersRequest: PostLettersRequest;
+
+ try {
+ postLettersRequest = PostLettersRequestSchema.parse(JSON.parse(body));
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new ValidationError(ApiErrorDetail.InvalidRequestBody, { cause: error});
+ }
+ else throw error;
+ }
+
+ if (postLettersRequest.data.length > maxUpdateItems) {
+ throw new ValidationError(ApiErrorDetail.InvalidRequestLettersToUpdate, { args: [maxUpdateItems]});
+ }
+
+ if( duplicateIdsExist(postLettersRequest) ) {
+ throw new ValidationError(ApiErrorDetail.InvalidRequestDuplicateLetterId);
+ }
+
+ await enqueueLetterUpdateRequests(postLettersRequest, commonHeadersResult.value.supplierId, commonHeadersResult.value.correlationId, deps);
+
+ return {
+ statusCode: 202,
+ body: ''
+ };
+
+ } catch (error) {
+ return processError(error, commonHeadersResult.value.correlationId, deps.logger);
+ }
+ };
+};
+
+function duplicateIdsExist(postLettersRequest: PostLettersRequest) {
+ const ids = postLettersRequest.data.map(item => item.id);
+ return new Set(ids).size !== ids.length;
+}
diff --git a/lambdas/api-handler/src/handlers/post-mi.ts b/lambdas/api-handler/src/handlers/post-mi.ts
index 526f849be..926fea6ce 100644
--- a/lambdas/api-handler/src/handlers/post-mi.ts
+++ b/lambdas/api-handler/src/handlers/post-mi.ts
@@ -2,7 +2,7 @@ import { APIGatewayProxyHandler } from "aws-lambda";
import { postMI as postMIOperation } from '../services/mi-operations';
import { ApiErrorDetail } from "../contracts/errors";
import { ValidationError } from "../errors";
-import { mapErrorToResponse } from "../mappers/error-mapper";
+import { processError } from "../mappers/error-mapper";
import { assertNotEmpty, validateCommonHeaders, validateIso8601Timestamp } from "../utils/validation";
import { PostMIRequest, PostMIRequestSchema } from "../contracts/mi";
import { mapToMI } from "../mappers/mi-mapper";
@@ -15,7 +15,7 @@ export function createPostMIHandler(deps: Deps): APIGatewayProxyHandler {
const commonHeadersResult = validateCommonHeaders(event.headers, deps);
if (!commonHeadersResult.ok) {
- return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
+ return processError(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
}
try {
@@ -41,7 +41,7 @@ export function createPostMIHandler(deps: Deps): APIGatewayProxyHandler {
};
} catch (error) {
- return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
+ return processError(error, commonHeadersResult.value.correlationId, deps.logger);
}
}
};
diff --git a/lambdas/api-handler/src/index.ts b/lambdas/api-handler/src/index.ts
index 38328703e..010d44b1b 100644
--- a/lambdas/api-handler/src/index.ts
+++ b/lambdas/api-handler/src/index.ts
@@ -3,6 +3,8 @@ import { createGetLetterHandler } from "./handlers/get-letter";
import { createGetLetterDataHandler } from "./handlers/get-letter-data";
import { createGetLettersHandler } from "./handlers/get-letters";
import { createPatchLetterHandler } from "./handlers/patch-letter";
+import { createPostLettersHandler } from "./handlers/post-letters";
+import { createLetterStatusUpdateHandler } from "./handlers/letter-status-update";
import { createPostMIHandler } from "./handlers/post-mi";
const container = createDependenciesContainer();
@@ -11,4 +13,7 @@ export const getLetter = createGetLetterHandler(container);
export const getLetterData = createGetLetterDataHandler(container);
export const getLetters = createGetLettersHandler(container);
export const patchLetter = createPatchLetterHandler(container);
+export const letterStatusUpdate = createLetterStatusUpdateHandler(container);
+export const postLetters = createPostLettersHandler(container);
+
export const postMI = createPostMIHandler(container);
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 5110e32bc..8a2d2c772 100644
--- a/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts
+++ b/lambdas/api-handler/src/mappers/__tests__/error-mapper.test.ts
@@ -1,13 +1,13 @@
-import { mapErrorToResponse } from "../error-mapper";
+import { processError } from "../error-mapper";
import { ValidationError, NotFoundError } from "../../errors";
import { ApiErrorDetail } from "../../contracts/errors";
import { Logger } from 'pino';
-describe("mapErrorToResponse", () => {
+describe("processError", () => {
it("should map ValidationError to InvalidRequest response", () => {
const err = new ValidationError(ApiErrorDetail.InvalidRequestLetterIdsMismatch);
- const res = mapErrorToResponse(err, 'correlationId', { info: jest.fn(), error: jest.fn() } as unknown as Logger);
+ const res = processError(err, 'correlationId', { info: jest.fn(), error: jest.fn() } as unknown as Logger);
expect(res.statusCode).toEqual(400);
expect(JSON.parse(res.body)).toEqual({
@@ -29,7 +29,7 @@ describe("mapErrorToResponse", () => {
it("should map NotFoundError to NotFound response", () => {
const err = new NotFoundError(ApiErrorDetail.NotFoundLetterId);
- const res = mapErrorToResponse(err, undefined, { info: jest.fn(), error: jest.fn() } as unknown as Logger);
+ const res = processError(err, undefined, { info: jest.fn(), error: jest.fn() } as unknown as Logger);
expect(res.statusCode).toEqual(404);
expect(JSON.parse(res.body)).toEqual({
@@ -51,7 +51,7 @@ describe("mapErrorToResponse", () => {
it("should map generic Error to InternalServerError response", () => {
const err = new Error("Something broke");
- const res = mapErrorToResponse(err, 'correlationId', { info: jest.fn(), error: jest.fn() } as unknown as Logger);
+ const res = processError(err, 'correlationId', { info: jest.fn(), error: jest.fn() } as unknown as Logger);
expect(res.statusCode).toEqual(500);
expect(JSON.parse(res.body)).toEqual({
@@ -73,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', { info: jest.fn(), error: jest.fn() } as unknown as Logger);
+ const res = processError(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 99493234b..180e0c8f9 100644
--- a/lambdas/api-handler/src/mappers/error-mapper.ts
+++ b/lambdas/api-handler/src/mappers/error-mapper.ts
@@ -1,42 +1,47 @@
import { APIGatewayProxyResult } from "aws-lambda";
import { NotFoundError, ValidationError } from "../errors";
-import { buildApiError, ApiErrorCode, ApiErrorDetail, ApiErrorTitle, ApiError, ApiErrorStatus } from "../contracts/errors";
+import { buildApiError, ApiErrorCode, ApiErrorTitle, ApiError, ApiErrorStatus, ErrorResponse } from "../contracts/errors";
import { v4 as uuid } from 'uuid';
import { Logger } from "pino";
-export interface ErrorResponse {
- errors: ApiError[];
+export function processError(error: unknown, correlationId: string | undefined, logger: Logger): APIGatewayProxyResult
+{
+ return mapToErrorResponse(logAndMapToApiError(error, correlationId, logger));
}
-export function mapErrorToResponse(error: unknown, correlationId: string | undefined, logger: Logger): APIGatewayProxyResult {
+export function logAndMapToApiError(error: unknown, correlationId: string | undefined, logger: Logger): ApiError {
if (error instanceof ValidationError) {
logger.info({ err: error }, `Validation error correlationId=${correlationId}`);
- return buildResponseFromErrorCode(ApiErrorCode.InvalidRequest, error.detail, correlationId);
+ return mapToApiError(ApiErrorCode.InvalidRequest, error.detail, correlationId);
} else if (error instanceof NotFoundError) {
logger.info({ err: error }, `Not found error correlationId=${correlationId}`);
- return buildResponseFromErrorCode(ApiErrorCode.NotFound, error.detail, correlationId);
+ return mapToApiError(ApiErrorCode.NotFound, error.detail, correlationId);
} else if (error instanceof Error) {
logger.error({ err: error }, `Internal server error correlationId=${correlationId}`);
- return buildResponseFromErrorCode(ApiErrorCode.InternalServerError, error.message, correlationId);
+ return mapToApiError(ApiErrorCode.InternalServerError, error.message, correlationId);
} else {
logger.error({ err: error }, `Internal server error (non-Error thrown) correlationId=${correlationId}`);
- return buildResponseFromErrorCode(ApiErrorCode.InternalServerError, "Unexpected error", correlationId);
+ return mapToApiError(ApiErrorCode.InternalServerError, "Unexpected error", correlationId);
}
}
-function buildResponseFromErrorCode(code: ApiErrorCode, detail: string, correlationId: string | undefined): APIGatewayProxyResult {
+function mapToErrorResponse(apiError: ApiError)
+{
+ return {
+ statusCode: +apiError.status,
+ body: JSON.stringify(buildErrorResponseFromError(apiError), null, 2)
+ };
+}
+
+function mapToApiError(code: ApiErrorCode, detail: string, correlationId: string | undefined): ApiError {
const id = correlationId ? correlationId : uuid();
- const responseError = buildApiError({
+ return buildApiError({
id,
code,
status: codeToStatus(code),
title: codeToTitle(code),
detail
});
- return {
- statusCode: +responseError.status,
- body: JSON.stringify(buildErrorResponseFromError(responseError), null, 2)
- };
}
function codeToStatus(code: ApiErrorCode): ApiErrorStatus {
diff --git a/lambdas/api-handler/src/mappers/letter-mapper.ts b/lambdas/api-handler/src/mappers/letter-mapper.ts
index e0f73b933..7d6efd734 100644
--- a/lambdas/api-handler/src/mappers/letter-mapper.ts
+++ b/lambdas/api-handler/src/mappers/letter-mapper.ts
@@ -1,7 +1,7 @@
import { LetterBase, LetterStatus } from "@internal/datastore";
-import { GetLetterResponse, GetLetterResponseSchema, GetLettersResponse, GetLettersResponseSchema, LetterDto, PatchLetterRequest, PatchLetterResponse, PatchLetterResponseSchema } from '../contracts/letters';
+import { GetLetterResponse, GetLetterResponseSchema, GetLettersResponse, GetLettersResponseSchema, LetterDto, PatchLetterRequest, PatchLetterResponse, PatchLetterResponseSchema, PostLettersRequest, PostLettersRequestResource } from '../contracts/letters';
-export function mapToLetterDto(request: PatchLetterRequest, supplierId: string): LetterDto {
+export function mapPatchLetterToDto(request: PatchLetterRequest, supplierId: string): LetterDto {
return {
id: request.data.id,
supplierId,
@@ -11,6 +11,16 @@ export function mapToLetterDto(request: PatchLetterRequest, supplierId: string):
};
}
+export function mapPostLetterResourceToDto(request: PostLettersRequestResource, supplierId: string): LetterDto {
+ return {
+ id: request.id,
+ supplierId,
+ status: LetterStatus.parse(request.attributes.status),
+ reasonCode: request.attributes.reasonCode,
+ reasonText: request.attributes.reasonText,
+ };
+}
+
export function mapToPatchLetterResponse(letter: LetterBase): PatchLetterResponse {
return PatchLetterResponseSchema.parse({
data: letterToResourceResponse(letter)
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 d8dd0a35c..5c4c47cdf 100644
--- a/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
+++ b/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts
@@ -1,7 +1,7 @@
import { Letter, LetterRepository } from '@internal/datastore';
import { Deps } from '../../config/deps';
-import { LetterDto } from '../../contracts/letters';
-import { getLetterById, getLetterDataUrl, getLettersForSupplier, patchLetterStatus } from '../letter-operations';
+import { LetterDto, PostLettersRequest } from '../../contracts/letters';
+import { enqueueLetterUpdateRequests, getLetterById, getLetterDataUrl, getLettersForSupplier, patchLetterStatus } from '../letter-operations';
import pino from 'pino';
jest.mock('@aws-sdk/s3-request-presigner', () => ({
@@ -16,6 +16,7 @@ jest.mock('@aws-sdk/client-s3', () => {
};
});
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
+import { SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs';
describe("getLetterIdsForSupplier", () => {
@@ -227,3 +228,142 @@ function makeLetter(id: string, status: Letter['status']) : Letter {
reasonText: "Reason text"
};
}
+
+describe('enqueueLetterUpdateRequests function', () => {
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should update the letter status successfully', async () => {
+
+ const updateLettersRequest : PostLettersRequest = {
+ data: [
+ {
+ id: 'id1',
+ type: 'Letter',
+ attributes: {
+ status: 'REJECTED',
+ reasonCode: 123,
+ reasonText: 'Reason text',
+ }
+ },
+ {
+ id: 'id2',
+ type: 'Letter',
+ attributes: {
+ status: 'ACCEPTED'
+ }
+ }
+ ]
+ };
+ const sqsClient = { send: jest.fn() } as unknown as SQSClient;
+ const logger = { error: jest.fn() } as unknown as pino.Logger;
+ const env = {
+ QUEUE_URL: 'sqsUrl'
+ };
+ const deps: Deps = { sqsClient, logger, env } as Deps;
+
+ const result = await enqueueLetterUpdateRequests(updateLettersRequest, 'supplier1', 'correlationId1', deps);
+
+ expect(result).toBeUndefined();
+
+ expect(deps.sqsClient.send).toHaveBeenNthCalledWith(1, expect.objectContaining({
+ input: {
+ QueueUrl: deps.env.QUEUE_URL,
+ MessageAttributes: {
+ CorrelationId: {
+ DataType: 'String',
+ StringValue: 'correlationId1',
+ }
+ },
+ MessageBody: JSON.stringify({
+ id: updateLettersRequest.data[0].id,
+ supplierId: 'supplier1',
+ status: updateLettersRequest.data[0].attributes.status,
+ reasonCode: updateLettersRequest.data[0].attributes.reasonCode,
+ reasonText: updateLettersRequest.data[0].attributes.reasonText
+ })
+ }
+ }));
+
+ expect(deps.sqsClient.send).toHaveBeenNthCalledWith(2, expect.objectContaining({
+ input: {
+ QueueUrl: deps.env.QUEUE_URL,
+ MessageAttributes: {
+ CorrelationId: {
+ DataType: 'String',
+ StringValue: 'correlationId1',
+ }
+ },
+ MessageBody: JSON.stringify({
+ id: updateLettersRequest.data[1].id,
+ supplierId: 'supplier1',
+ status: updateLettersRequest.data[1].attributes.status
+ })
+ }
+ }));
+ });
+
+ it('should log error if enqueueing fails', async () => {
+
+ const mockError = new Error('error');
+
+ const updateLettersRequest : PostLettersRequest = {
+ data: [
+ {
+ id: 'id1',
+ type: 'Letter',
+ attributes: {
+ status: 'ACCEPTED'
+ }
+ },
+ {
+ id: 'id2',
+ type: 'Letter',
+ attributes: {
+ status: 'REJECTED'
+ }
+ }
+ ]
+ };
+ const sqsClient = { send: jest.fn()
+ .mockRejectedValue(mockError)
+ .mockResolvedValueOnce({ MessageId: 'm1' })
+ } as unknown as SQSClient;
+ const logger = { error: jest.fn() } as unknown as pino.Logger;
+ const env = {
+ QUEUE_URL: 'sqsUrl'
+ };
+ const deps: Deps = { sqsClient, logger, env } as Deps;
+
+ const result = await enqueueLetterUpdateRequests(updateLettersRequest, 'supplier1', 'correlationId1', deps);
+
+ expect(result).toBeUndefined();
+
+ expect(deps.sqsClient.send).toHaveBeenNthCalledWith(2, expect.objectContaining({
+ input: {
+ QueueUrl: deps.env.QUEUE_URL,
+ MessageAttributes: {
+ CorrelationId: {
+ DataType: 'String',
+ StringValue: 'correlationId1',
+ }
+ },
+ MessageBody: JSON.stringify({
+ id: updateLettersRequest.data[1].id,
+ supplierId: 'supplier1',
+ status: updateLettersRequest.data[1].attributes.status
+ })
+ }
+ }));
+
+ expect(deps.logger.error).toHaveBeenCalledWith({
+ err: mockError,
+ correlationId: 'correlationId1',
+ letterId: 'id2',
+ letterStatus: 'REJECTED',
+ supplierId: 'supplier1'
+ }, 'Error enqueuing letter status update');
+ });
+});
diff --git a/lambdas/api-handler/src/services/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts
index 035aba2aa..839a1f819 100644
--- a/lambdas/api-handler/src/services/letter-operations.ts
+++ b/lambdas/api-handler/src/services/letter-operations.ts
@@ -1,10 +1,11 @@
-import { LetterBase, LetterRepository } from '@internal/datastore'
+import { LetterBase, LetterRepository } from '@internal/datastore';
import { NotFoundError, ValidationError } from '../errors';
-import { LetterDto, PatchLetterResponse } from '../contracts/letters';
-import { mapToPatchLetterResponse } from '../mappers/letter-mapper';
+import { LetterDto, PatchLetterResponse, PostLettersRequest } from '../contracts/letters';
+import { mapPostLetterResourceToDto, 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 { SendMessageCommand } from "@aws-sdk/client-sqs";
import { Deps } from '../config/deps';
@@ -48,9 +49,6 @@ export const patchLetterStatus = async (letterToUpdate: LetterDto, letterId: str
return mapToPatchLetterResponse(updatedLetter);
}
-function isNotFoundError(error: any) {
- return error instanceof Error && /^Letter with id \w+ not found for supplier \w+$/.test(error.message);
-}
export const getLetterDataUrl = async (supplierId: string, letterId: string, deps: Deps): Promise => {
@@ -60,7 +58,7 @@ export const getLetterDataUrl = async (supplierId: string, letterId: string, dep
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)) {
+ if (isNotFoundError(error)) {
throw new NotFoundError(ApiErrorDetail.NotFoundLetterId);
}
throw error;
@@ -80,3 +78,33 @@ async function getDownloadUrl(s3Uri: string, s3Client: S3Client, expiry: number)
return await getSignedUrl(s3Client, command, { expiresIn: expiry });
}
+
+export async function enqueueLetterUpdateRequests(postLettersRequest: PostLettersRequest, supplierId: string, correlationId: string, deps: Deps) {
+
+ const tasks = postLettersRequest.data.map(async (request) => {
+ try {
+ const command = new SendMessageCommand({
+ QueueUrl: deps.env.QUEUE_URL,
+ MessageAttributes: {
+ CorrelationId: { DataType: 'String', StringValue: correlationId },
+ },
+ MessageBody: JSON.stringify(mapPostLetterResourceToDto(request, supplierId)),
+ });
+ await deps.sqsClient.send(command);
+ } catch (err) {
+ deps.logger.error({
+ err,
+ correlationId: correlationId,
+ letterId: request.id,
+ letterStatus: request.attributes.status,
+ supplierId: supplierId
+ }, 'Error enqueuing letter status update');
+ }
+ });
+
+ await Promise.all(tasks);
+}
+
+function isNotFoundError(error: any) {
+ return error instanceof Error && /^Letter with id \w+ not found for supplier \w+$/.test(error.message);
+}
diff --git a/lambdas/api-handler/src/utils/validation.ts b/lambdas/api-handler/src/utils/validation.ts
index f86b4fffe..57aebb458 100644
--- a/lambdas/api-handler/src/utils/validation.ts
+++ b/lambdas/api-handler/src/utils/validation.ts
@@ -2,6 +2,7 @@ import { APIGatewayProxyEventHeaders } from 'aws-lambda';
import { ValidationError } from '../errors';
import { ApiErrorDetail } from '../contracts/errors';
import { Deps } from '../config/deps';
+import { EnvVars } from '../config/env';
export function assertNotEmpty(
value: T | null | undefined,
@@ -84,3 +85,14 @@ export function validateIso8601Timestamp(timestamp: string) {
throw new ValidationError(ApiErrorDetail.InvalidRequestTimestamp);
}
}
+
+export function requireEnvVar(
+ envs: EnvVars,
+ name: T
+): NonNullable {
+ const value = envs[name];
+ if (value === undefined || value === null || value === "") {
+ throw new Error(`Missing required environment variable: ${name}`);
+ }
+ return value as NonNullable;
+}
diff --git a/package-lock.json b/package-lock.json
index 69f093794..92d1968af 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2049,6 +2049,7 @@
"name": "nhs-notify-supplier-api-handler",
"version": "0.0.1",
"dependencies": {
+ "@aws-sdk/client-sqs": "^3.925.0",
"@internal/datastore": "*",
"@internal/helpers": "*",
"esbuild": "^0.25.11",
@@ -4238,6 +4239,533 @@
],
"license": "MIT"
},
+ "node_modules/@aws-sdk/client-sqs": {
+ "version": "3.925.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.925.0.tgz",
+ "integrity": "sha512-4ts+M2M7b7qB2zRa1pVvqXGFKrKSjOsI6zRd+DqbHUz7MvSFY8qVm1joH2ByqSLdK1XMdD6cwDj7Nn6uWNWziQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/credential-provider-node": "3.925.0",
+ "@aws-sdk/middleware-host-header": "3.922.0",
+ "@aws-sdk/middleware-logger": "3.922.0",
+ "@aws-sdk/middleware-recursion-detection": "3.922.0",
+ "@aws-sdk/middleware-sdk-sqs": "3.922.0",
+ "@aws-sdk/middleware-user-agent": "3.922.0",
+ "@aws-sdk/region-config-resolver": "3.925.0",
+ "@aws-sdk/types": "3.922.0",
+ "@aws-sdk/util-endpoints": "3.922.0",
+ "@aws-sdk/util-user-agent-browser": "3.922.0",
+ "@aws-sdk/util-user-agent-node": "3.922.0",
+ "@smithy/config-resolver": "^4.4.2",
+ "@smithy/core": "^3.17.2",
+ "@smithy/fetch-http-handler": "^5.3.5",
+ "@smithy/hash-node": "^4.2.4",
+ "@smithy/invalid-dependency": "^4.2.4",
+ "@smithy/md5-js": "^4.2.4",
+ "@smithy/middleware-content-length": "^4.2.4",
+ "@smithy/middleware-endpoint": "^4.3.6",
+ "@smithy/middleware-retry": "^4.4.6",
+ "@smithy/middleware-serde": "^4.2.4",
+ "@smithy/middleware-stack": "^4.2.4",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/node-http-handler": "^4.4.4",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/smithy-client": "^4.9.2",
+ "@smithy/types": "^4.8.1",
+ "@smithy/url-parser": "^4.2.4",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.5",
+ "@smithy/util-defaults-mode-node": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.4",
+ "@smithy/util-middleware": "^4.2.4",
+ "@smithy/util-retry": "^4.2.4",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/client-sso": {
+ "version": "3.925.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.925.0.tgz",
+ "integrity": "sha512-ixC9CyXe/mBo1X+bzOxIIzsdBYzM+klWoHUYzwnPMrXhpDrMjj8D24R/FPqrDnhoYYXiyS4BApRLpeymsFJq2Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/middleware-host-header": "3.922.0",
+ "@aws-sdk/middleware-logger": "3.922.0",
+ "@aws-sdk/middleware-recursion-detection": "3.922.0",
+ "@aws-sdk/middleware-user-agent": "3.922.0",
+ "@aws-sdk/region-config-resolver": "3.925.0",
+ "@aws-sdk/types": "3.922.0",
+ "@aws-sdk/util-endpoints": "3.922.0",
+ "@aws-sdk/util-user-agent-browser": "3.922.0",
+ "@aws-sdk/util-user-agent-node": "3.922.0",
+ "@smithy/config-resolver": "^4.4.2",
+ "@smithy/core": "^3.17.2",
+ "@smithy/fetch-http-handler": "^5.3.5",
+ "@smithy/hash-node": "^4.2.4",
+ "@smithy/invalid-dependency": "^4.2.4",
+ "@smithy/middleware-content-length": "^4.2.4",
+ "@smithy/middleware-endpoint": "^4.3.6",
+ "@smithy/middleware-retry": "^4.4.6",
+ "@smithy/middleware-serde": "^4.2.4",
+ "@smithy/middleware-stack": "^4.2.4",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/node-http-handler": "^4.4.4",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/smithy-client": "^4.9.2",
+ "@smithy/types": "^4.8.1",
+ "@smithy/url-parser": "^4.2.4",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.5",
+ "@smithy/util-defaults-mode-node": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.4",
+ "@smithy/util-middleware": "^4.2.4",
+ "@smithy/util-retry": "^4.2.4",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/core": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz",
+ "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.922.0",
+ "@aws-sdk/xml-builder": "3.921.0",
+ "@smithy/core": "^3.17.2",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/signature-v4": "^5.3.4",
+ "@smithy/smithy-client": "^4.9.2",
+ "@smithy/types": "^4.8.1",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-middleware": "^4.2.4",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz",
+ "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz",
+ "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/fetch-http-handler": "^5.3.5",
+ "@smithy/node-http-handler": "^4.4.4",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/smithy-client": "^4.9.2",
+ "@smithy/types": "^4.8.1",
+ "@smithy/util-stream": "^4.5.5",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.925.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.925.0.tgz",
+ "integrity": "sha512-TOs/UkKWwXrSPolRTChpDUQjczw6KqbbanF0EzjUm3sp/AS1ThOQCKuTTdaOBZXkCIJdvRmZjF3adccE3rAoXg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/credential-provider-env": "3.922.0",
+ "@aws-sdk/credential-provider-http": "3.922.0",
+ "@aws-sdk/credential-provider-process": "3.922.0",
+ "@aws-sdk/credential-provider-sso": "3.925.0",
+ "@aws-sdk/credential-provider-web-identity": "3.925.0",
+ "@aws-sdk/nested-clients": "3.925.0",
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/credential-provider-imds": "^4.2.4",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/shared-ini-file-loader": "^4.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.925.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.925.0.tgz",
+ "integrity": "sha512-+T9mnnTY73MLkVxsk5RtzE4fv7GnMhR7iXhL/yTusf1zLfA09uxlA9VCz6tWxm5rHcO4ZN0x4hnqqDhM+DB5KQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.922.0",
+ "@aws-sdk/credential-provider-http": "3.922.0",
+ "@aws-sdk/credential-provider-ini": "3.925.0",
+ "@aws-sdk/credential-provider-process": "3.922.0",
+ "@aws-sdk/credential-provider-sso": "3.925.0",
+ "@aws-sdk/credential-provider-web-identity": "3.925.0",
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/credential-provider-imds": "^4.2.4",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/shared-ini-file-loader": "^4.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz",
+ "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/shared-ini-file-loader": "^4.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.925.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.925.0.tgz",
+ "integrity": "sha512-aZlUC6LRsOMDvIu0ifF62mTjL3KGzclWu5XBBN8eLDAYTdhqMxv3HyrqWoiHnGZnZGaVU+II+qsVoeBnGOwHow==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-sso": "3.925.0",
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/token-providers": "3.925.0",
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/shared-ini-file-loader": "^4.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.925.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.925.0.tgz",
+ "integrity": "sha512-dR34s8Sfd1wJBzIuvRFO2FCnLmYD8iwPWrdXWI2ZypFt1EQR8jeQ20mnS+UOCoR5Z0tY6wJqEgTXKl4KuZ+DUg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/nested-clients": "3.925.0",
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/shared-ini-file-loader": "^4.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/middleware-host-header": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz",
+ "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/middleware-logger": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz",
+ "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/middleware-recursion-detection": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz",
+ "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.922.0",
+ "@aws/lambda-invoke-store": "^0.1.1",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/middleware-user-agent": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz",
+ "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/types": "3.922.0",
+ "@aws-sdk/util-endpoints": "3.922.0",
+ "@smithy/core": "^3.17.2",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/nested-clients": {
+ "version": "3.925.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.925.0.tgz",
+ "integrity": "sha512-Fc8QhH+1YzGQb5aWQUX6gRnKSzUZ9p3p/muqXIgYBL8RSd5O6hSPhDTyrOWE247zFlOjVlAlEnoTMJKarH0cIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/middleware-host-header": "3.922.0",
+ "@aws-sdk/middleware-logger": "3.922.0",
+ "@aws-sdk/middleware-recursion-detection": "3.922.0",
+ "@aws-sdk/middleware-user-agent": "3.922.0",
+ "@aws-sdk/region-config-resolver": "3.925.0",
+ "@aws-sdk/types": "3.922.0",
+ "@aws-sdk/util-endpoints": "3.922.0",
+ "@aws-sdk/util-user-agent-browser": "3.922.0",
+ "@aws-sdk/util-user-agent-node": "3.922.0",
+ "@smithy/config-resolver": "^4.4.2",
+ "@smithy/core": "^3.17.2",
+ "@smithy/fetch-http-handler": "^5.3.5",
+ "@smithy/hash-node": "^4.2.4",
+ "@smithy/invalid-dependency": "^4.2.4",
+ "@smithy/middleware-content-length": "^4.2.4",
+ "@smithy/middleware-endpoint": "^4.3.6",
+ "@smithy/middleware-retry": "^4.4.6",
+ "@smithy/middleware-serde": "^4.2.4",
+ "@smithy/middleware-stack": "^4.2.4",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/node-http-handler": "^4.4.4",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/smithy-client": "^4.9.2",
+ "@smithy/types": "^4.8.1",
+ "@smithy/url-parser": "^4.2.4",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.5",
+ "@smithy/util-defaults-mode-node": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.4",
+ "@smithy/util-middleware": "^4.2.4",
+ "@smithy/util-retry": "^4.2.4",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/region-config-resolver": {
+ "version": "3.925.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.925.0.tgz",
+ "integrity": "sha512-FOthcdF9oDb1pfQBRCfWPZhJZT5wqpvdAS5aJzB1WDZ+6EuaAhLzLH/fW1slDunIqq1PSQGG3uSnVglVVOvPHQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/config-resolver": "^4.4.2",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/token-providers": {
+ "version": "3.925.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.925.0.tgz",
+ "integrity": "sha512-F4Oibka1W5YYDeL+rGt/Hg3NLjOzrJdmuZOE0OFQt/U6dnJwYmYi2gFqduvZnZcD1agNm37mh7/GUq1zvKS6ig==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.922.0",
+ "@aws-sdk/nested-clients": "3.925.0",
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/shared-ini-file-loader": "^4.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/types": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz",
+ "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz",
+ "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/types": "^4.8.1",
+ "@smithy/url-parser": "^4.2.4",
+ "@smithy/util-endpoints": "^3.2.4",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz",
+ "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/types": "^4.8.1",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz",
+ "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-user-agent": "3.922.0",
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/xml-builder": {
+ "version": "3.921.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz",
+ "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.8.1",
+ "fast-xml-parser": "5.2.5",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/node_modules/@aws/lambda-invoke-store": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz",
+ "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sqs/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==",
+ "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/client-sqs/node_modules/strnum": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
+ "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@aws-sdk/client-sso": {
"version": "3.864.0",
"license": "Apache-2.0",
@@ -4825,6 +5353,36 @@
],
"license": "MIT"
},
+ "node_modules/@aws-sdk/middleware-sdk-sqs": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.922.0.tgz",
+ "integrity": "sha512-OJKwy247mgBVWJeSbIwz+QRVmW2L/qA5eKQivNNiSNADeETZKcJupY/3UoyrhQP08dZnFpst1Qs75E47v/tubQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.922.0",
+ "@smithy/smithy-client": "^4.9.2",
+ "@smithy/types": "^4.8.1",
+ "@smithy/util-hex-encoding": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-sdk-sqs/node_modules/@aws-sdk/types": {
+ "version": "3.922.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz",
+ "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.8.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@aws-sdk/middleware-ssec": {
"version": "3.893.0",
"license": "Apache-2.0",
@@ -9160,10 +9718,12 @@
}
},
"node_modules/@smithy/abort-controller": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.4.tgz",
+ "integrity": "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9192,13 +9752,16 @@
}
},
"node_modules/@smithy/config-resolver": {
- "version": "4.3.0",
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.2.tgz",
+ "integrity": "sha512-4Jys0ni2tB2VZzgslbEgszZyMdTkPOFGA8g+So/NjR8oy6Qwaq4eSwsrRI+NMtb0Dq4kqCzGUu/nGUx7OM/xfw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/types": "^4.8.1",
"@smithy/util-config-provider": "^4.2.0",
- "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-endpoints": "^3.2.4",
+ "@smithy/util-middleware": "^4.2.4",
"tslib": "^2.6.2"
},
"engines": {
@@ -9206,16 +9769,18 @@
}
},
"node_modules/@smithy/core": {
- "version": "3.15.0",
+ "version": "3.17.2",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.2.tgz",
+ "integrity": "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/middleware-serde": "^4.2.0",
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/middleware-serde": "^4.2.4",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/types": "^4.8.1",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
- "@smithy/util-middleware": "^4.2.0",
- "@smithy/util-stream": "^4.5.0",
+ "@smithy/util-middleware": "^4.2.4",
+ "@smithy/util-stream": "^4.5.5",
"@smithy/util-utf8": "^4.2.0",
"@smithy/uuid": "^1.1.0",
"tslib": "^2.6.2"
@@ -9225,13 +9790,15 @@
}
},
"node_modules/@smithy/credential-provider-imds": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.4.tgz",
+ "integrity": "sha512-YVNMjhdz2pVto5bRdux7GMs0x1m0Afz3OcQy/4Yf9DH4fWOtroGH7uLvs7ZmDyoBJzLdegtIPpXrpJOZWvUXdw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.0",
- "@smithy/property-provider": "^4.2.0",
- "@smithy/types": "^4.6.0",
- "@smithy/url-parser": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/types": "^4.8.1",
+ "@smithy/url-parser": "^4.2.4",
"tslib": "^2.6.2"
},
"engines": {
@@ -9299,12 +9866,14 @@
}
},
"node_modules/@smithy/fetch-http-handler": {
- "version": "5.3.1",
+ "version": "5.3.5",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.5.tgz",
+ "integrity": "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/querystring-builder": "^4.2.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/querystring-builder": "^4.2.4",
+ "@smithy/types": "^4.8.1",
"@smithy/util-base64": "^4.3.0",
"tslib": "^2.6.2"
},
@@ -9326,10 +9895,12 @@
}
},
"node_modules/@smithy/hash-node": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.4.tgz",
+ "integrity": "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.8.1",
"@smithy/util-buffer-from": "^4.2.0",
"@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
@@ -9351,10 +9922,12 @@
}
},
"node_modules/@smithy/invalid-dependency": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.4.tgz",
+ "integrity": "sha512-z6aDLGiHzsMhbS2MjetlIWopWz//K+mCoPXjW6aLr0mypF+Y7qdEh5TyJ20Onf9FbWHiWl4eC+rITdizpnXqOw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9372,11 +9945,13 @@
}
},
"node_modules/@smithy/md5-js": {
- "version": "4.1.1",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.4.tgz",
+ "integrity": "sha512-h7kzNWZuMe5bPnZwKxhVbY1gan5+TZ2c9JcVTHCygB14buVGOZxLl+oGfpY2p2Xm48SFqEWdghpvbBdmaz3ncQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.5.0",
- "@smithy/util-utf8": "^4.1.0",
+ "@smithy/types": "^4.8.1",
+ "@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -9384,11 +9959,13 @@
}
},
"node_modules/@smithy/middleware-content-length": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.4.tgz",
+ "integrity": "sha512-hJRZuFS9UsElX4DJSJfoX4M1qXRH+VFiLMUnhsWvtOOUWRNvvOfDaUSdlNbjwv1IkpVjj/Rd/O59Jl3nhAcxow==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9396,16 +9973,18 @@
}
},
"node_modules/@smithy/middleware-endpoint": {
- "version": "4.3.1",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/core": "^3.15.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",
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.6.tgz",
+ "integrity": "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.17.2",
+ "@smithy/middleware-serde": "^4.2.4",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/shared-ini-file-loader": "^4.3.4",
+ "@smithy/types": "^4.8.1",
+ "@smithy/url-parser": "^4.2.4",
+ "@smithy/util-middleware": "^4.2.4",
"tslib": "^2.6.2"
},
"engines": {
@@ -9413,16 +9992,18 @@
}
},
"node_modules/@smithy/middleware-retry": {
- "version": "4.4.1",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/node-config-provider": "^4.3.0",
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/service-error-classification": "^4.2.0",
- "@smithy/smithy-client": "^4.7.1",
- "@smithy/types": "^4.6.0",
- "@smithy/util-middleware": "^4.2.0",
- "@smithy/util-retry": "^4.2.0",
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.6.tgz",
+ "integrity": "sha512-OhLx131znrEDxZPAvH/OYufR9d1nB2CQADyYFN4C3V/NQS7Mg4V6uvxHC/Dr96ZQW8IlHJTJ+vAhKt6oxWRndA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/service-error-classification": "^4.2.4",
+ "@smithy/smithy-client": "^4.9.2",
+ "@smithy/types": "^4.8.1",
+ "@smithy/util-middleware": "^4.2.4",
+ "@smithy/util-retry": "^4.2.4",
"@smithy/uuid": "^1.1.0",
"tslib": "^2.6.2"
},
@@ -9431,11 +10012,13 @@
}
},
"node_modules/@smithy/middleware-serde": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.4.tgz",
+ "integrity": "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9443,10 +10026,12 @@
}
},
"node_modules/@smithy/middleware-stack": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.4.tgz",
+ "integrity": "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9454,12 +10039,14 @@
}
},
"node_modules/@smithy/node-config-provider": {
- "version": "4.3.0",
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.4.tgz",
+ "integrity": "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/property-provider": "^4.2.0",
- "@smithy/shared-ini-file-loader": "^4.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/shared-ini-file-loader": "^4.3.4",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9467,13 +10054,15 @@
}
},
"node_modules/@smithy/node-http-handler": {
- "version": "4.3.0",
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.4.tgz",
+ "integrity": "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/abort-controller": "^4.2.0",
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/querystring-builder": "^4.2.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/abort-controller": "^4.2.4",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/querystring-builder": "^4.2.4",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9481,10 +10070,12 @@
}
},
"node_modules/@smithy/property-provider": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.4.tgz",
+ "integrity": "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9492,10 +10083,12 @@
}
},
"node_modules/@smithy/protocol-http": {
- "version": "5.3.0",
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.4.tgz",
+ "integrity": "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9503,10 +10096,12 @@
}
},
"node_modules/@smithy/querystring-builder": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.4.tgz",
+ "integrity": "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.8.1",
"@smithy/util-uri-escape": "^4.2.0",
"tslib": "^2.6.2"
},
@@ -9515,10 +10110,12 @@
}
},
"node_modules/@smithy/querystring-parser": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.4.tgz",
+ "integrity": "sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9526,20 +10123,24 @@
}
},
"node_modules/@smithy/service-error-classification": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.4.tgz",
+ "integrity": "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0"
+ "@smithy/types": "^4.8.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/shared-ini-file-loader": {
- "version": "4.3.0",
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.4.tgz",
+ "integrity": "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9547,14 +10148,16 @@
}
},
"node_modules/@smithy/signature-v4": {
- "version": "5.3.0",
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.4.tgz",
+ "integrity": "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/is-array-buffer": "^4.2.0",
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/types": "^4.8.1",
"@smithy/util-hex-encoding": "^4.2.0",
- "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.4",
"@smithy/util-uri-escape": "^4.2.0",
"@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
@@ -9564,15 +10167,17 @@
}
},
"node_modules/@smithy/smithy-client": {
- "version": "4.7.1",
+ "version": "4.9.2",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.2.tgz",
+ "integrity": "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.15.0",
- "@smithy/middleware-endpoint": "^4.3.1",
- "@smithy/middleware-stack": "^4.2.0",
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/types": "^4.6.0",
- "@smithy/util-stream": "^4.5.0",
+ "@smithy/core": "^3.17.2",
+ "@smithy/middleware-endpoint": "^4.3.6",
+ "@smithy/middleware-stack": "^4.2.4",
+ "@smithy/protocol-http": "^5.3.4",
+ "@smithy/types": "^4.8.1",
+ "@smithy/util-stream": "^4.5.5",
"tslib": "^2.6.2"
},
"engines": {
@@ -9580,7 +10185,9 @@
}
},
"node_modules/@smithy/types": {
- "version": "4.6.0",
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.1.tgz",
+ "integrity": "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -9590,11 +10197,13 @@
}
},
"node_modules/@smithy/url-parser": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.4.tgz",
+ "integrity": "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/querystring-parser": "^4.2.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/querystring-parser": "^4.2.4",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9655,12 +10264,14 @@
}
},
"node_modules/@smithy/util-defaults-mode-browser": {
- "version": "4.3.0",
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.5.tgz",
+ "integrity": "sha512-GwaGjv/QLuL/QHQaqhf/maM7+MnRFQQs7Bsl6FlaeK6lm6U7mV5AAnVabw68cIoMl5FQFyKK62u7RWRzWL25OQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/property-provider": "^4.2.0",
- "@smithy/smithy-client": "^4.7.1",
- "@smithy/types": "^4.6.0",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/smithy-client": "^4.9.2",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9668,15 +10279,17 @@
}
},
"node_modules/@smithy/util-defaults-mode-node": {
- "version": "4.2.1",
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.8.tgz",
+ "integrity": "sha512-gIoTf9V/nFSIZ0TtgDNLd+Ws59AJvijmMDYrOozoMHPJaG9cMRdqNO50jZTlbM6ydzQYY8L/mQ4tKSw/TB+s6g==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/config-resolver": "^4.3.0",
- "@smithy/credential-provider-imds": "^4.2.0",
- "@smithy/node-config-provider": "^4.3.0",
- "@smithy/property-provider": "^4.2.0",
- "@smithy/smithy-client": "^4.7.1",
- "@smithy/types": "^4.6.0",
+ "@smithy/config-resolver": "^4.4.2",
+ "@smithy/credential-provider-imds": "^4.2.4",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/property-provider": "^4.2.4",
+ "@smithy/smithy-client": "^4.9.2",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9684,11 +10297,13 @@
}
},
"node_modules/@smithy/util-endpoints": {
- "version": "3.2.0",
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.4.tgz",
+ "integrity": "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/node-config-provider": "^4.3.4",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9706,10 +10321,12 @@
}
},
"node_modules/@smithy/util-middleware": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.4.tgz",
+ "integrity": "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9717,11 +10334,13 @@
}
},
"node_modules/@smithy/util-retry": {
- "version": "4.2.0",
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.4.tgz",
+ "integrity": "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/service-error-classification": "^4.2.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/service-error-classification": "^4.2.4",
+ "@smithy/types": "^4.8.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9729,12 +10348,14 @@
}
},
"node_modules/@smithy/util-stream": {
- "version": "4.5.0",
+ "version": "4.5.5",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.5.tgz",
+ "integrity": "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/fetch-http-handler": "^5.3.1",
- "@smithy/node-http-handler": "^4.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/fetch-http-handler": "^5.3.5",
+ "@smithy/node-http-handler": "^4.4.4",
+ "@smithy/types": "^4.8.1",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-buffer-from": "^4.2.0",
"@smithy/util-hex-encoding": "^4.2.0",
diff --git a/sandbox/api/openapi.yaml b/sandbox/api/openapi.yaml
index ec700064f..0f80ee80b 100644
--- a/sandbox/api/openapi.yaml
+++ b/sandbox/api/openapi.yaml
@@ -459,6 +459,8 @@ paths:
It is not possible to update a letter to status of `PENDING`.
+ The request should not contain multiple letter objects with the same ID.
+
Optionally a `reasonCode` and `reasonText` explaining the status (for example, validation failures) can be included in the request body for each update.
operationId: postLetters
parameters:
@@ -561,23 +563,23 @@ paths:
example: 11C46F5F-CDEF-4865-94B2-0EE0EDCC26DA
type: string
style: simple
- "404":
+ "400":
content:
application/vnd.api+json:
examples:
- error-not-found:
+ error-bad-request-invalid-request-body:
value:
errors:
- - code: NOTIFY_RESOURCE_NOT_FOUND
- detail: No resource found with that ID
+ - code: NOTIFY_INVALID_REQUEST
+ detail: The request body is invalid
id: rrt-1931948104716186917-c-geu2-10664-3111479-3.0
links:
about: https://digital.nhs.uk/developer/api-catalogue/nhs-notify-supplier
- status: "404"
- title: Resource not found
+ status: "400"
+ title: Invalid request
schema:
$ref: "#/components/schemas/listLetters_400_response"
- description: Resource not found
+ description: "Bad request, invalid input data"
"429":
content:
application/vnd.api+json:
diff --git a/sandbox/services/LetterService.js b/sandbox/services/LetterService.js
index 609c5a2f9..a603d5fa6 100644
--- a/sandbox/services/LetterService.js
+++ b/sandbox/services/LetterService.js
@@ -111,7 +111,7 @@ const patchLetter = ({ xRequestId, id, body, xCorrelationId }) => new Promise(
* xRequestId String Unique request identifier, in the format of a GUID
* postLettersRequest PostLettersRequest
* xCorrelationId String An optional ID which you can use to track transactions across multiple systems. It can take any value, but we recommend avoiding `.` characters. If not provided in the request, NHS Notify will default to a system generated ID in its place. The ID will be returned in a response header. (optional)
-* returns listLetters_200_response
+* returns 202
* */
const postLetters = ({ xRequestId, body, xCorrelationId }) => new Promise(
async (resolve, reject) => {
diff --git a/sandbox/utils/ResponseProvider.js b/sandbox/utils/ResponseProvider.js
index 1e091637b..44dfff0dc 100644
--- a/sandbox/utils/ResponseProvider.js
+++ b/sandbox/utils/ResponseProvider.js
@@ -93,10 +93,10 @@ async function patchLetterResponse(request) {
}
async function postLettersResponse(request) {
- const patchLetterFileMap = {
+ const postLettersFileMap = {
'data/examples/postLetter/requests/postLetters.json': {responsePath: 'data/examples/postLetter/responses/postLetters.json', responseCode: 202},
};
- return await mapExampleResponse(request, patchLetterFileMap);
+ return await mapExampleResponse(request, postLettersFileMap);
}
async function postMIResponse(request) {
diff --git a/specification/api/components/documentation/postLetters.md b/specification/api/components/documentation/postLetters.md
index b067b6881..e1b2b1273 100644
--- a/specification/api/components/documentation/postLetters.md
+++ b/specification/api/components/documentation/postLetters.md
@@ -25,6 +25,8 @@ Allowed `status` values that can be used to are:
It is not possible to update a letter to status of `PENDING`.
+The request should not contain multiple letter objects with the same ID.
+
Optionally a `reasonCode` and `reasonText` explaining the status (for example, validation failures) can be included in the request body for each update.
### Example Error Codes