diff --git a/.vscode/settings.json b/.vscode/settings.json
index d7c02400c..3f98619f9 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -10,5 +10,6 @@
"**/Thumbs.db": true,
".github": false,
".vscode": false
- }
+ },
+ "typescript.tsdk": "node_modules/typescript/lib"
}
diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md
index 75faa887f..8b5b020c9 100644
--- a/infrastructure/terraform/components/api/README.md
+++ b/infrastructure/terraform/components/api/README.md
@@ -63,6 +63,7 @@ No requirements.
| [s3bucket\_test\_letters](#module\_s3bucket\_test\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-s3bucket.zip | n/a |
| [sqs\_letter\_updates](#module\_sqs\_letter\_updates) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a |
| [supplier\_ssl](#module\_supplier\_ssl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-ssl.zip | n/a |
+| [update\_letter\_queue](#module\_update\_letter\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
| [upsert\_letter](#module\_upsert\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a |
## Outputs
diff --git a/infrastructure/terraform/components/api/ddb_table_letter_queue.tf b/infrastructure/terraform/components/api/ddb_table_letter_queue.tf
new file mode 100644
index 000000000..83cf1f24a
--- /dev/null
+++ b/infrastructure/terraform/components/api/ddb_table_letter_queue.tf
@@ -0,0 +1,44 @@
+resource "aws_dynamodb_table" "letter-queue" {
+ name = "${local.csi}-letter-queue"
+ billing_mode = "PAY_PER_REQUEST"
+
+ hash_key = "supplierId"
+ range_key = "letterId"
+
+ ttl {
+ attribute_name = "ttl"
+ enabled = true
+ }
+
+ local_secondary_index {
+ name = "timestamp-index"
+ range_key = "queueTimestamp"
+ projection_type = "ALL"
+ }
+
+ attribute {
+ name = "supplierId"
+ type = "S"
+ }
+
+ attribute {
+ name = "letterId"
+ type = "S"
+ }
+
+ attribute {
+ name = "queueTimestamp"
+ type = "S"
+ }
+
+ point_in_time_recovery {
+ enabled = true
+ }
+
+ tags = merge(
+ local.default_tags,
+ {
+ NHSE-Enable-Dynamo-Backup-Acct = "True"
+ }
+ )
+}
diff --git a/infrastructure/terraform/components/api/event_source_mapping_update_letter_queue.tf b/infrastructure/terraform/components/api/event_source_mapping_update_letter_queue.tf
new file mode 100644
index 000000000..9546f4ef7
--- /dev/null
+++ b/infrastructure/terraform/components/api/event_source_mapping_update_letter_queue.tf
@@ -0,0 +1,11 @@
+resource "aws_lambda_event_source_mapping" "update_letter_queue_kinesis" {
+ event_source_arn = aws_kinesis_stream.letter_change_stream.arn
+ function_name = module.update_letter_queue.function_arn
+ starting_position = "LATEST"
+ batch_size = 10
+ maximum_batching_window_in_seconds = 1
+
+ depends_on = [
+ module.update_letter_queue # ensures update letter queue lambda exists
+ ]
+}
diff --git a/infrastructure/terraform/components/api/module_lambda_update_letter_queue.tf b/infrastructure/terraform/components/api/module_lambda_update_letter_queue.tf
new file mode 100644
index 000000000..f25bf7a33
--- /dev/null
+++ b/infrastructure/terraform/components/api/module_lambda_update_letter_queue.tf
@@ -0,0 +1,70 @@
+module "update_letter_queue" {
+ source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip"
+
+ function_name = "update-letter-queue"
+ description = "Populates the letter queue table with new pending letters from the letter change stream"
+
+ 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.update_letter_queue_lambda.json
+ }
+
+ function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
+ function_code_base_path = local.aws_lambda_functions_dir_path
+ function_code_dir = "update-letter-queue/dist"
+ function_include_common = true
+ handler_function_name = "handler"
+ runtime = "nodejs22.x"
+ memory = 512
+ timeout = 29
+ log_level = var.log_level
+
+ force_lambda_code_deploy = var.force_lambda_code_deploy
+ enable_lambda_insights = false
+
+ 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, {
+ LETTER_QUEUE_TABLE_NAME = aws_dynamodb_table.letter-queue.name,
+ LETTER_QUEUE_TTL_HOURS = 168 # 7 days
+ })
+}
+
+data "aws_iam_policy_document" "update_letter_queue_lambda" {
+ statement {
+ sid = "AllowDynamoDBWrite"
+ effect = "Allow"
+
+ actions = [
+ "dynamodb:PutItem",
+ ]
+
+ resources = [
+ aws_dynamodb_table.letter-queue.arn,
+ "${aws_dynamodb_table.letter-queue.arn}/index/*"
+ ]
+ }
+
+ statement {
+ sid = "AllowKinesisGet"
+ effect = "Allow"
+
+ actions = [
+ "kinesis:GetRecords",
+ ]
+
+ resources = [
+ aws_kinesis_stream.letter_change_stream.arn
+ ]
+ }
+}
diff --git a/internal/datastore/src/__test__/db.ts b/internal/datastore/src/__test__/db.ts
index 1d364b9f5..f382add62 100644
--- a/internal/datastore/src/__test__/db.ts
+++ b/internal/datastore/src/__test__/db.ts
@@ -30,9 +30,11 @@ export async function setupDynamoDBContainer() {
region: "us-west-2",
endpoint,
lettersTableName: "letters",
+ letterQueueTableName: "letter-queue",
miTableName: "management-info",
suppliersTableName: "suppliers",
lettersTtlHours: 1,
+ letterQueueTtlHours: 1,
miTtlHours: 1,
};
@@ -118,6 +120,32 @@ const createSupplierTableCommand = new CreateTableCommand({
],
});
+const createLetterQueueTableCommand = new CreateTableCommand({
+ TableName: "letter-queue",
+ BillingMode: "PAY_PER_REQUEST",
+ KeySchema: [
+ { AttributeName: "supplierId", KeyType: "HASH" }, // Partition key
+ { AttributeName: "letterId", KeyType: "RANGE" }, // Sort key
+ ],
+ LocalSecondaryIndexes: [
+ {
+ IndexName: "timestamp-index",
+ KeySchema: [
+ { AttributeName: "supplierId", KeyType: "HASH" }, // Partition key for LSI
+ { AttributeName: "queueTimestamp", KeyType: "RANGE" }, // Sort key for LSI
+ ],
+ Projection: {
+ ProjectionType: "ALL",
+ },
+ },
+ ],
+ AttributeDefinitions: [
+ { AttributeName: "supplierId", AttributeType: "S" },
+ { AttributeName: "letterId", AttributeType: "S" },
+ { AttributeName: "queueTimestamp", AttributeType: "S" },
+ ],
+});
+
export async function createTables(context: DBContext) {
const { ddbClient } = context;
@@ -126,26 +154,22 @@ export async function createTables(context: DBContext) {
await ddbClient.send(createMITableCommand);
await ddbClient.send(createSupplierTableCommand);
+ await ddbClient.send(createLetterQueueTableCommand);
}
export async function deleteTables(context: DBContext) {
const { ddbClient } = context;
- await ddbClient.send(
- new DeleteTableCommand({
- TableName: "letters",
- }),
- );
-
- await ddbClient.send(
- new DeleteTableCommand({
- TableName: "management-info",
- }),
- );
-
- await ddbClient.send(
- new DeleteTableCommand({
- TableName: "suppliers",
- }),
- );
+ for (const tableName of [
+ "letters",
+ "management-info",
+ "suppliers",
+ "letter-queue",
+ ]) {
+ await ddbClient.send(
+ new DeleteTableCommand({
+ TableName: tableName,
+ }),
+ );
+ }
}
diff --git a/internal/datastore/src/__test__/letter-queue-repository.test.ts b/internal/datastore/src/__test__/letter-queue-repository.test.ts
new file mode 100644
index 000000000..b4ec6b641
--- /dev/null
+++ b/internal/datastore/src/__test__/letter-queue-repository.test.ts
@@ -0,0 +1,104 @@
+import { Logger } from "pino";
+import {
+ DBContext,
+ createTables,
+ deleteTables,
+ setupDynamoDBContainer,
+} from "./db";
+import LetterQueueRepository from "../letter-queue-repository";
+import { InsertPendingLetter } from "../types";
+import { createTestLogger } from "./logs";
+
+function createLetter(letterId = "letter1"): InsertPendingLetter {
+ return {
+ letterId,
+ supplierId: "supplier1",
+ specificationId: "specification1",
+ groupId: "group1",
+ };
+}
+
+// Database tests can take longer, especially with setup and teardown
+jest.setTimeout(30_000);
+
+describe("LetterQueueRepository", () => {
+ let db: DBContext;
+ let letterQueueRepository: LetterQueueRepository;
+ let logger: Logger;
+
+ beforeAll(async () => {
+ db = await setupDynamoDBContainer();
+ });
+
+ beforeEach(async () => {
+ await createTables(db);
+ ({ logger } = createTestLogger());
+
+ letterQueueRepository = new LetterQueueRepository(
+ db.docClient,
+ logger,
+ db.config,
+ );
+ });
+
+ afterEach(async () => {
+ await deleteTables(db);
+ jest.useRealTimers();
+ });
+
+ afterAll(async () => {
+ await db.container.stop();
+ });
+
+ function assertTtl(ttl: number, before: number, after: number) {
+ const expectedLower = Math.floor(
+ before / 1000 + 60 * 60 * db.config.letterQueueTtlHours,
+ );
+ const expectedUpper = Math.floor(
+ after / 1000 + 60 * 60 * db.config.lettersTtlHours,
+ );
+ expect(ttl).toBeGreaterThanOrEqual(expectedLower);
+ expect(ttl).toBeLessThanOrEqual(expectedUpper);
+ }
+
+ describe("putLetter", () => {
+ it("adds a letter to the database", async () => {
+ const before = Date.now();
+
+ const pendingLetter =
+ await letterQueueRepository.putLetter(createLetter());
+
+ const after = Date.now();
+
+ const timestampInMillis = new Date(
+ pendingLetter.queueTimestamp,
+ ).valueOf();
+ expect(timestampInMillis).toBeGreaterThanOrEqual(before);
+ expect(timestampInMillis).toBeLessThanOrEqual(after);
+ assertTtl(pendingLetter.ttl, before, after);
+ });
+
+ it("throws an error when creating a letter which already exists", async () => {
+ await letterQueueRepository.putLetter(createLetter());
+ await expect(
+ letterQueueRepository.putLetter(createLetter()),
+ ).rejects.toThrow(
+ "Letter with id letter1 already exists for supplier supplier1",
+ );
+ });
+
+ it("rethrows errors from DynamoDB when creating a letter", async () => {
+ const misconfiguredRepository = new LetterQueueRepository(
+ db.docClient,
+ logger,
+ {
+ ...db.config,
+ letterQueueTableName: "nonexistent-table",
+ },
+ );
+ await expect(
+ misconfiguredRepository.putLetter(createLetter()),
+ ).rejects.toThrow("Cannot do operations on a non-existent table");
+ });
+ });
+});
diff --git a/internal/datastore/src/config.ts b/internal/datastore/src/config.ts
index 6440942e8..4066101ad 100644
--- a/internal/datastore/src/config.ts
+++ b/internal/datastore/src/config.ts
@@ -2,8 +2,10 @@ export type DatastoreConfig = {
region: string;
endpoint?: string;
lettersTableName: string;
+ letterQueueTableName: string;
miTableName: string;
suppliersTableName: string;
lettersTtlHours: number;
+ letterQueueTtlHours: number;
miTtlHours: number;
};
diff --git a/internal/datastore/src/index.ts b/internal/datastore/src/index.ts
index 53d52cd6b..850d50398 100644
--- a/internal/datastore/src/index.ts
+++ b/internal/datastore/src/index.ts
@@ -2,4 +2,5 @@ export * from "./types";
export * from "./mi-repository";
export * from "./letter-repository";
export * from "./supplier-repository";
+export { default as LetterQueueRepository } from "./letter-queue-repository";
export { default as DBHealthcheck } from "./healthcheck";
diff --git a/internal/datastore/src/letter-queue-repository.ts b/internal/datastore/src/letter-queue-repository.ts
new file mode 100644
index 000000000..319e47ab7
--- /dev/null
+++ b/internal/datastore/src/letter-queue-repository.ts
@@ -0,0 +1,72 @@
+import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
+import { Logger } from "pino";
+import { createHash } from "node:crypto";
+import {
+ InsertPendingLetter,
+ PendingLetter,
+ PendingLetterSchema,
+} from "./types";
+
+type LetterQueueRepositoryConfig = {
+ letterQueueTableName: string;
+ letterQueueTtlHours: number;
+};
+
+export function createSha256Hash(
+ letter: Omit,
+): string {
+ // Use an array so that hash does not depend on insertion order
+ const dataToHash = JSON.stringify([
+ letter.groupId,
+ letter.letterId,
+ letter.queueTimestamp,
+ letter.specificationId,
+ letter.supplierId,
+ ]);
+ return createHash("sha256").update(dataToHash).digest("hex");
+}
+
+export default class LetterQueueRepository {
+ constructor(
+ readonly ddbClient: DynamoDBDocumentClient,
+ readonly log: Logger,
+ readonly config: LetterQueueRepositoryConfig,
+ ) {}
+
+ async putLetter(
+ insertPendingLetter: InsertPendingLetter,
+ ): Promise {
+ const queueTimestamp = new Date().toISOString();
+ const letterWithTimestamp = {
+ ...insertPendingLetter,
+ queueTimestamp, // needs to be an ISO timestamp as Db sorts alphabetically
+ };
+ const pendingLetter: PendingLetter = {
+ ...letterWithTimestamp,
+ sha256hash: createSha256Hash(letterWithTimestamp),
+ ttl: Math.floor(
+ Date.now() / 1000 + 60 * 60 * this.config.letterQueueTtlHours,
+ ),
+ };
+ try {
+ await this.ddbClient.send(
+ new PutCommand({
+ TableName: this.config.letterQueueTableName,
+ Item: pendingLetter,
+ ConditionExpression: "attribute_not_exists(letterId)", // Ensures the supplierId/letterId combination is unique
+ }),
+ );
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ error.name === "ConditionalCheckFailedException"
+ ) {
+ throw new Error(
+ `Letter with id ${pendingLetter.letterId} already exists for supplier ${pendingLetter.supplierId}`,
+ );
+ }
+ throw error;
+ }
+ return PendingLetterSchema.parse(pendingLetter);
+ }
+}
diff --git a/internal/datastore/src/types.ts b/internal/datastore/src/types.ts
index a0b9f719c..129255658 100644
--- a/internal/datastore/src/types.ts
+++ b/internal/datastore/src/types.ts
@@ -73,6 +73,23 @@ export type UpdateLetter = {
reasonText?: string;
};
+export const PendingLetterSchema = z.object({
+ supplierId: idRef(SupplierSchema, "id"),
+ letterId: idRef(LetterSchema, "id"),
+ queueTimestamp: z.string().describe("Secondary index SK"),
+ specificationId: z.string(),
+ groupId: z.string(),
+ sha256hash: z.string(),
+ ttl: z.int(),
+});
+
+export type PendingLetter = z.infer;
+
+export type InsertPendingLetter = Omit<
+ PendingLetter,
+ "ttl" | "queueTimestamp" | "sha256hash"
+>;
+
export const MISchemaBase = z.object({
id: z.string(),
lineItem: z.string(),
diff --git a/lambdas/update-letter-queue/.eslintignore b/lambdas/update-letter-queue/.eslintignore
new file mode 100644
index 000000000..6d6ccf00d
--- /dev/null
+++ b/lambdas/update-letter-queue/.eslintignore
@@ -0,0 +1,3 @@
+dist
+node_modules
+.reports
diff --git a/lambdas/update-letter-queue/.gitignore b/lambdas/update-letter-queue/.gitignore
new file mode 100644
index 000000000..6d6ccf00d
--- /dev/null
+++ b/lambdas/update-letter-queue/.gitignore
@@ -0,0 +1,3 @@
+dist
+node_modules
+.reports
diff --git a/lambdas/update-letter-queue/jest.config.ts b/lambdas/update-letter-queue/jest.config.ts
new file mode 100644
index 000000000..e173d5e31
--- /dev/null
+++ b/lambdas/update-letter-queue/jest.config.ts
@@ -0,0 +1,65 @@
+export const baseJestConfig = {
+ preset: "ts-jest",
+ extensionsToTreatAsEsm: [".ts"],
+ transform: {
+ "^.+\\.ts$": [
+ "ts-jest",
+ {
+ useESM: true,
+ },
+ ],
+ },
+
+ // Automatically clear mock calls, instances, contexts and results before every test
+ clearMocks: true,
+
+ // Indicates whether the coverage information should be collected while executing the test
+ collectCoverage: true,
+
+ // The directory where Jest should output its coverage files
+ coverageDirectory: "./.reports/unit/coverage",
+
+ // Indicates which provider should be used to instrument code for coverage
+ coverageProvider: "babel",
+
+ coverageThreshold: {
+ global: {
+ branches: 100,
+ functions: 100,
+ lines: 100,
+ statements: -10,
+ },
+ },
+
+ coveragePathIgnorePatterns: ["/__tests__/"],
+ testPathIgnorePatterns: [".build"],
+ testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],
+
+ // Use this configuration option to add custom reporters to Jest
+ reporters: [
+ "default",
+ [
+ "jest-html-reporter",
+ {
+ pageTitle: "Test Report",
+ outputPath: "./.reports/unit/test-report.html",
+ includeFailureMsg: true,
+ },
+ ],
+ ],
+
+ // The test environment that will be used for testing
+ testEnvironment: "node",
+};
+
+const utilsJestConfig = {
+ ...baseJestConfig,
+
+ testEnvironment: "node",
+
+ coveragePathIgnorePatterns: [
+ ...(baseJestConfig.coveragePathIgnorePatterns ?? []),
+ ],
+};
+
+export default utilsJestConfig;
diff --git a/lambdas/update-letter-queue/package.json b/lambdas/update-letter-queue/package.json
new file mode 100644
index 000000000..6ae220596
--- /dev/null
+++ b/lambdas/update-letter-queue/package.json
@@ -0,0 +1,32 @@
+{
+ "dependencies": {
+ "@aws-sdk/client-dynamodb": "^3.984.0",
+ "@aws-sdk/lib-dynamodb": "^3.984.0",
+ "@aws-sdk/util-dynamodb": "^3.943.0",
+ "@internal/datastore": "*",
+ "aws-embedded-metrics": "^4.2.1",
+ "aws-lambda": "^1.0.6",
+ "esbuild": "0.27.2",
+ "pino": "^10.3.0",
+ "zod": "^4.1.13"
+ },
+ "devDependencies": {
+ "@tsconfig/node22": "^22.0.2",
+ "@types/aws-lambda": "^8.10.148",
+ "@types/jest": "^30.0.0",
+ "jest": "^30.2.0",
+ "jest-mock-extended": "^4.0.0",
+ "ts-jest": "^29.4.0",
+ "typescript": "^5.8.3"
+ },
+ "name": "nhs-notify-supplier-api-update-letter-queue",
+ "private": true,
+ "scripts": {
+ "lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:unit": "jest",
+ "typecheck": "tsc --noEmit"
+ },
+ "version": "0.0.1"
+}
diff --git a/lambdas/update-letter-queue/src/__tests__/types.ts b/lambdas/update-letter-queue/src/__tests__/types.ts
new file mode 100644
index 000000000..292af5e1e
--- /dev/null
+++ b/lambdas/update-letter-queue/src/__tests__/types.ts
@@ -0,0 +1,12 @@
+import { z } from "zod";
+import { LetterStatus } from "internal/datastore/src";
+
+export const IncomingLetterSchema = z.object({
+ id: z.string(),
+ status: LetterStatus,
+ specificationId: z.string(),
+ supplierId: z.string(),
+ groupId: z.string(),
+});
+
+export type IncomingLetter = z.infer;
diff --git a/lambdas/update-letter-queue/src/__tests__/update-letter-queue.test.ts b/lambdas/update-letter-queue/src/__tests__/update-letter-queue.test.ts
new file mode 100644
index 000000000..c28c12393
--- /dev/null
+++ b/lambdas/update-letter-queue/src/__tests__/update-letter-queue.test.ts
@@ -0,0 +1,249 @@
+import { Letter, LetterQueueRepository } from "internal/datastore/src";
+import { mockDeep } from "jest-mock-extended";
+import pino from "pino";
+import {
+ Context,
+ DynamoDBRecord,
+ KinesisStreamEvent,
+ KinesisStreamRecordPayload,
+} from "aws-lambda";
+import { Deps } from "../deps";
+import createHandler from "../update-letter-queue";
+import { EnvVars } from "../env";
+import { LetterStatus } from "../../../api-handler/src/contracts/letters";
+
+const mockPutMetric = jest.fn();
+const mockSetNamespace = jest.fn();
+
+jest.mock("aws-embedded-metrics", () => ({
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
+ metricScope: (fn: Function) =>
+ fn({
+ putMetric: mockPutMetric,
+ setNamespace: mockSetNamespace,
+ }),
+ Unit: { Count: "Count" },
+}));
+
+const mockedDeps: jest.Mocked = {
+ letterQueueRepository: {
+ putLetter: jest.fn(),
+ } as unknown as LetterQueueRepository,
+ logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger,
+ env: {} as unknown as EnvVars,
+} as Deps;
+
+function generateLetter(status: LetterStatus, id?: string): Letter {
+ return {
+ id: id || "1",
+ status,
+ specificationId: "spec1",
+ supplierId: "supplier1",
+ groupId: "group1",
+ url: "https://example.com/letter.pdf",
+ createdAt: "2026-01-01T00:00:00.000Z",
+ updatedAt: "2026-01-01T00:00:00.000Z",
+ supplierStatus: "supplier1#PENDING",
+ supplierStatusSk: "2026-01-01T00:00:00.000Z#1",
+ ttl: 1_735_689_600,
+ source: "test-source",
+ subject: "test-subject",
+ billingRef: "billing-ref-1",
+ };
+}
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+describe("update-letter-queue Lambda", () => {
+ describe("filtering", () => {
+ it("processes new pending letters and persists them in the letter queue table", async () => {
+ const handler = createHandler(mockedDeps);
+ const newLetter = generateLetter("PENDING");
+
+ const testData = generateKinesisEvent([generateInsertRecord(newLetter)]);
+ const result = await handler(testData, mockDeep(), jest.fn());
+
+ expect(mockedDeps.letterQueueRepository.putLetter).toHaveBeenCalledWith({
+ supplierId: "supplier1",
+ letterId: "1",
+ specificationId: "spec1",
+ groupId: "group1",
+ });
+ expect(result.batchItemFailures).toEqual([]);
+ });
+
+ it("does not publish updates", async () => {
+ const handler = createHandler(mockedDeps);
+ const oldLetter = generateLetter("PENDING");
+ const newLetter = generateLetter("PENDING");
+
+ const testData = generateKinesisEvent([
+ generateModifyRecord(oldLetter, newLetter),
+ ]);
+ const result = await handler(testData, mockDeep(), jest.fn());
+
+ expect(mockedDeps.letterQueueRepository.putLetter).not.toHaveBeenCalled();
+ expect(result.batchItemFailures).toEqual([]);
+ });
+
+ it("does not publish non-PENDING letters", async () => {
+ const handler = createHandler(mockedDeps);
+ const newLetter = generateLetter("PRINTED");
+
+ const testData = generateKinesisEvent([generateInsertRecord(newLetter)]);
+ const result = await handler(testData, mockDeep(), jest.fn());
+
+ expect(mockedDeps.letterQueueRepository.putLetter).not.toHaveBeenCalled();
+ expect(result.batchItemFailures).toEqual([]);
+ });
+ });
+
+ describe("Error handling", () => {
+ it("rejects invalid letter data", async () => {
+ const handler = createHandler(mockedDeps);
+ const newLetter = { id: "1", status: "PENDING" } as Letter;
+
+ const testData = generateKinesisEvent([generateInsertRecord(newLetter)]);
+ await expect(
+ handler(testData, mockDeep(), jest.fn()),
+ ).rejects.toThrow();
+
+ expect(mockedDeps.letterQueueRepository.putLetter).not.toHaveBeenCalled();
+ });
+
+ it("returns on the first failure", async () => {
+ const handler = createHandler(mockedDeps);
+ const newLetter1 = generateLetter("PENDING", "1");
+ const newLetter2 = generateLetter("PENDING", "2");
+ (mockedDeps.letterQueueRepository.putLetter as jest.Mock)
+ .mockRejectedValueOnce({})
+ .mockResolvedValueOnce({});
+
+ const testData = generateKinesisEvent([
+ generateInsertRecord(newLetter1),
+ generateInsertRecord(newLetter2),
+ ]);
+ const result = await handler(testData, mockDeep(), jest.fn());
+
+ expect(mockedDeps.letterQueueRepository.putLetter).toHaveBeenCalledTimes(
+ 1,
+ );
+ expect(result.batchItemFailures).toEqual([{ itemIdentifier: "1" }]);
+ });
+ });
+});
+
+describe("Metrics", () => {
+ it("emits success metrics when all letters are processed successfully", async () => {
+ const handler = createHandler(mockedDeps);
+ const newLetter1 = generateLetter("PENDING", "1");
+ const newLetter2 = generateLetter("PENDING", "2");
+
+ const testData = generateKinesisEvent([
+ generateInsertRecord(newLetter1),
+ generateInsertRecord(newLetter2),
+ ]);
+ await handler(testData, mockDeep(), jest.fn());
+
+ expect(mockSetNamespace).toHaveBeenCalledWith("update-letter-queue");
+ expect(mockPutMetric).toHaveBeenCalledWith(
+ "letters queued successfully",
+ 2,
+ "Count",
+ );
+ expect(mockPutMetric).toHaveBeenCalledWith(
+ "letters queued failed",
+ 0,
+ "Count",
+ );
+ });
+
+ it("emits failure metrics when a letter fails to process", async () => {
+ const handler = createHandler(mockedDeps);
+ const newLetter1 = generateLetter("PENDING", "1");
+ const newLetter2 = generateLetter("PENDING", "2");
+ (mockedDeps.letterQueueRepository.putLetter as jest.Mock)
+ .mockResolvedValueOnce({})
+ .mockRejectedValueOnce(new Error("DynamoDB error"));
+
+ const testData = generateKinesisEvent([
+ generateInsertRecord(newLetter1),
+ generateInsertRecord(newLetter2),
+ ]);
+ await handler(testData, mockDeep(), jest.fn());
+
+ expect(mockSetNamespace).toHaveBeenCalledWith("update-letter-queue");
+ expect(mockPutMetric).toHaveBeenCalledWith(
+ "letters queued successfully",
+ 1,
+ "Count",
+ );
+ expect(mockPutMetric).toHaveBeenCalledWith(
+ "letters queued failed",
+ 1,
+ "Count",
+ );
+ });
+
+ it("emits zero success metrics when no pending letters are in the batch", async () => {
+ const handler = createHandler(mockedDeps);
+ const newLetter = generateLetter("PRINTED");
+
+ const testData = generateKinesisEvent([generateInsertRecord(newLetter)]);
+ await handler(testData, mockDeep(), jest.fn());
+
+ expect(mockSetNamespace).toHaveBeenCalledWith("update-letter-queue");
+ expect(mockPutMetric).toHaveBeenCalledWith(
+ "letters queued successfully",
+ 0,
+ "Count",
+ );
+ expect(mockPutMetric).toHaveBeenCalledWith(
+ "letters queued failed",
+ 0,
+ "Count",
+ );
+ });
+});
+
+function generateKinesisEvent(letterEvents: object[]): KinesisStreamEvent {
+ const records = letterEvents
+ .map((letter) => Buffer.from(JSON.stringify(letter)).toString("base64"))
+ .map(
+ (data) =>
+ ({ kinesis: { data } }) as unknown as KinesisStreamRecordPayload,
+ );
+
+ return { Records: records } as unknown as KinesisStreamEvent;
+}
+
+function generateInsertRecord(newLetter: Letter): DynamoDBRecord {
+ return {
+ eventName: "INSERT",
+ dynamodb: { NewImage: mapToImage(newLetter) },
+ };
+}
+
+function generateModifyRecord(
+ oldLetter: Letter,
+ newLetter: Letter,
+): DynamoDBRecord {
+ return {
+ eventName: "MODIFY",
+ dynamodb: {
+ OldImage: mapToImage(oldLetter),
+ NewImage: mapToImage(newLetter),
+ },
+ };
+}
+
+function mapToImage(oldLetter: Letter) {
+ return Object.fromEntries(
+ Object.entries(oldLetter).map(([key, value]) => [
+ key,
+ typeof value === "string" ? { S: value } : { N: String(value) },
+ ]),
+ );
+}
diff --git a/lambdas/update-letter-queue/src/deps.ts b/lambdas/update-letter-queue/src/deps.ts
new file mode 100644
index 000000000..e1fd14f39
--- /dev/null
+++ b/lambdas/update-letter-queue/src/deps.ts
@@ -0,0 +1,32 @@
+import pino from "pino";
+import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
+import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
+import { LetterQueueRepository } from "@internal/datastore";
+import { EnvVars, envVars } from "./env";
+
+export type Deps = {
+ letterQueueRepository: LetterQueueRepository;
+ logger: pino.Logger;
+ env: EnvVars;
+};
+
+function createDynamoDBDocumentClient(): DynamoDBDocumentClient {
+ const client = new DynamoDBClient({});
+ return DynamoDBDocumentClient.from(client);
+}
+
+export function createDependenciesContainer(): Deps {
+ const log = pino();
+ const ddbClient = createDynamoDBDocumentClient();
+
+ const letterQueueRepository = new LetterQueueRepository(ddbClient, log, {
+ letterQueueTableName: envVars.LETTER_QUEUE_TABLE_NAME,
+ letterQueueTtlHours: envVars.LETTER_QUEUE_TTL_HOURS,
+ });
+
+ return {
+ letterQueueRepository,
+ logger: log,
+ env: envVars,
+ };
+}
diff --git a/lambdas/update-letter-queue/src/env.ts b/lambdas/update-letter-queue/src/env.ts
new file mode 100644
index 000000000..1902cea9e
--- /dev/null
+++ b/lambdas/update-letter-queue/src/env.ts
@@ -0,0 +1,10 @@
+import { z } from "zod";
+
+const EnvVarsSchema = z.object({
+ LETTER_QUEUE_TABLE_NAME: z.string(),
+ LETTER_QUEUE_TTL_HOURS: z.coerce.number().int(),
+});
+
+export type EnvVars = z.infer;
+
+export const envVars = EnvVarsSchema.parse(process.env);
diff --git a/lambdas/update-letter-queue/src/index.ts b/lambdas/update-letter-queue/src/index.ts
new file mode 100644
index 000000000..1278c8ee9
--- /dev/null
+++ b/lambdas/update-letter-queue/src/index.ts
@@ -0,0 +1,7 @@
+import createHandler from "./update-letter-queue";
+import { createDependenciesContainer } from "./deps";
+
+const container = createDependenciesContainer();
+
+// eslint-disable-next-line import-x/prefer-default-export
+export const handler = createHandler(container);
diff --git a/lambdas/update-letter-queue/src/update-letter-queue.ts b/lambdas/update-letter-queue/src/update-letter-queue.ts
new file mode 100644
index 000000000..65e2d0dd0
--- /dev/null
+++ b/lambdas/update-letter-queue/src/update-letter-queue.ts
@@ -0,0 +1,147 @@
+import {
+ DynamoDBRecord,
+ Handler,
+ KinesisStreamEvent,
+ KinesisStreamRecord,
+} from "aws-lambda";
+import { unmarshall } from "@aws-sdk/util-dynamodb";
+import { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics";
+import { InsertPendingLetter, Letter, LetterSchema } from "@internal/datastore";
+import { Deps } from "./deps";
+
+export default function createHandler(deps: Deps): Handler {
+ return metricScope((metrics: MetricsLogger) => {
+ return async (streamEvent: KinesisStreamEvent) => {
+ let successCount = 0;
+
+ deps.logger.info({ description: "Received event", streamEvent });
+ deps.logger.info({
+ description: "Number of records",
+ count: streamEvent.Records?.length || 0,
+ });
+
+ const ddbRecords: DynamoDBRecord[] = streamEvent.Records.map((record) =>
+ extractPayload(record, deps),
+ );
+
+ const newPendingLetters = ddbRecords
+ .filter((record) => filterRecord(record, deps))
+ .map((element) => extractNewLetter(element))
+ .map((element) => mapLetterToPendingLetter(element));
+
+ for (const pendingLetter of newPendingLetters) {
+ try {
+ deps.logger.info({
+ description: "Persisting pending letter",
+ pendingLetter,
+ });
+ await deps.letterQueueRepository.putLetter(pendingLetter);
+ successCount += 1;
+ } catch (error) {
+ deps.logger.error({
+ description: "Error persisting pending letter",
+ error,
+ pendingLetter,
+ });
+ recordProcessing(deps, successCount, 1, metrics);
+ // If we get a failure, return immediately without processing the remaining records. Since we are
+ // working with a Kinesis stream, AWS will retry from the point of failure and no records will be lost.
+ // See https://docs.aws.amazon.com/lambda/latest/dg/example_serverless_Kinesis_Lambda_batch_item_failures_section.html
+ return {
+ batchItemFailures: [{ itemIdentifier: pendingLetter.letterId }],
+ };
+ }
+ }
+
+ recordProcessing(deps, successCount, 0, metrics);
+ return { batchItemFailures: [] };
+ };
+ });
+}
+
+function recordProcessing(
+ deps: Deps,
+ successCount: number,
+ failureCount: number,
+ metrics: MetricsLogger,
+) {
+ deps.logger.info({
+ description: "Processing complete",
+ successCount,
+ failureCount,
+ totalProcessed: successCount + failureCount,
+ });
+
+ emitMetrics(metrics, successCount, failureCount);
+}
+
+function emitMetrics(
+ metrics: MetricsLogger,
+ successCount: number,
+ failureCount: number,
+) {
+ metrics.setNamespace(
+ process.env.AWS_LAMBDA_FUNCTION_NAME || "update-letter-queue",
+ );
+ metrics.putMetric("letters queued successfully", successCount, Unit.Count);
+ metrics.putMetric("letters queued failed", failureCount, Unit.Count);
+}
+
+function filterRecord(record: DynamoDBRecord, deps: Deps): boolean {
+ const isInsert = record.eventName === "INSERT";
+ const newImage = record.dynamodb?.NewImage;
+ const isPending = newImage?.status?.S === "PENDING";
+
+ const allowEvent = isInsert && isPending;
+
+ deps.logger.info({
+ description: "Filtering record",
+ eventName: record.eventName,
+ eventId: record.eventID,
+ status: newImage?.status?.S,
+ allowEvent,
+ });
+
+ return allowEvent;
+}
+
+function extractPayload(
+ record: KinesisStreamRecord,
+ deps: Deps,
+): DynamoDBRecord {
+ try {
+ deps.logger.info({
+ description: "Processing Kinesis record",
+ recordId: record.kinesis.sequenceNumber,
+ });
+
+ // Kinesis data is base64 encoded
+ const payload = Buffer.from(record.kinesis.data, "base64").toString("utf8");
+ deps.logger.info({ description: "Decoded payload", payload });
+
+ const jsonParsed = JSON.parse(payload);
+ deps.logger.info({ description: "Extracted dynamoDBRecord", jsonParsed });
+ return jsonParsed;
+ } catch (error) {
+ deps.logger.error({
+ description: "Error extracting payload",
+ error,
+ record,
+ });
+ throw error;
+ }
+}
+
+function extractNewLetter(record: DynamoDBRecord): Letter {
+ const newImage = record.dynamodb?.NewImage!;
+ return LetterSchema.parse(unmarshall(newImage as any));
+}
+
+function mapLetterToPendingLetter(letter: Letter): InsertPendingLetter {
+ return {
+ supplierId: letter.supplierId,
+ letterId: letter.id,
+ specificationId: letter.specificationId,
+ groupId: letter.groupId,
+ };
+}
diff --git a/lambdas/update-letter-queue/tsconfig.json b/lambdas/update-letter-queue/tsconfig.json
new file mode 100644
index 000000000..f3fa0970e
--- /dev/null
+++ b/lambdas/update-letter-queue/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "esModuleInterop": true,
+ "resolveJsonModule": true
+ },
+ "extends": "../../tsconfig.base.json",
+ "include": [
+ "src/**/*",
+ "jest.config.ts"
+ ]
+}
diff --git a/package-lock.json b/package-lock.json
index a63b758e2..ae19fa777 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -122,24 +122,26 @@
"eslint": ">=9.0.0"
}
},
- "internal/datastore/node_modules/pino": {
- "version": "9.14.0",
+ "internal/datastore/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "internal/datastore/node_modules/picomatch": {
+ "version": "4.0.3",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@pinojs/redact": "^0.4.0",
- "atomic-sleep": "^1.0.0",
- "on-exit-leak-free": "^2.1.0",
- "pino-abstract-transport": "^2.0.0",
- "pino-std-serializers": "^7.0.0",
- "process-warning": "^5.0.0",
- "quick-format-unescaped": "^4.0.3",
- "real-require": "^0.2.0",
- "safe-stable-stringify": "^2.3.1",
- "sonic-boom": "^4.0.1",
- "thread-stream": "^3.0.0"
+ "engines": {
+ "node": ">=12"
},
- "bin": {
- "pino": "bin.js"
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
"internal/events": {
@@ -184,6 +186,28 @@
"eslint": ">=9.0.0"
}
},
+ "internal/events/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "internal/events/node_modules/picomatch": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"internal/helpers": {
"name": "@internal/helpers",
"version": "0.1.0",
@@ -225,24 +249,26 @@
"eslint": ">=9.0.0"
}
},
- "internal/helpers/node_modules/pino": {
- "version": "9.14.0",
+ "internal/helpers/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "internal/helpers/node_modules/picomatch": {
+ "version": "4.0.3",
+ "dev": true,
"license": "MIT",
- "dependencies": {
- "@pinojs/redact": "^0.4.0",
- "atomic-sleep": "^1.0.0",
- "on-exit-leak-free": "^2.1.0",
- "pino-abstract-transport": "^2.0.0",
- "pino-std-serializers": "^7.0.0",
- "process-warning": "^5.0.0",
- "quick-format-unescaped": "^4.0.3",
- "real-require": "^0.2.0",
- "safe-stable-stringify": "^2.3.1",
- "sonic-boom": "^4.0.1",
- "thread-stream": "^3.0.0"
+ "engines": {
+ "node": ">=12"
},
- "bin": {
- "pino": "bin.js"
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
"lambdas/api-handler": {
@@ -273,61 +299,6 @@
"zod": "^4.1.11"
}
},
- "lambdas/api-handler/node_modules/argparse": {
- "version": "1.0.10",
- "license": "MIT",
- "dependencies": {
- "sprintf-js": "~1.0.2"
- }
- },
- "lambdas/api-handler/node_modules/aws-lambda": {
- "version": "1.0.7",
- "license": "MIT",
- "dependencies": {
- "aws-sdk": "^2.814.0",
- "commander": "^3.0.2",
- "js-yaml": "^3.14.1",
- "watchpack": "^2.0.0-beta.10"
- },
- "bin": {
- "lambda": "bin/lambda"
- }
- },
- "lambdas/api-handler/node_modules/commander": {
- "version": "3.0.2",
- "license": "MIT"
- },
- "lambdas/api-handler/node_modules/js-yaml": {
- "version": "3.14.2",
- "license": "MIT",
- "dependencies": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "lambdas/api-handler/node_modules/pino": {
- "version": "9.14.0",
- "license": "MIT",
- "dependencies": {
- "@pinojs/redact": "^0.4.0",
- "atomic-sleep": "^1.0.0",
- "on-exit-leak-free": "^2.1.0",
- "pino-abstract-transport": "^2.0.0",
- "pino-std-serializers": "^7.0.0",
- "process-warning": "^5.0.0",
- "quick-format-unescaped": "^4.0.3",
- "real-require": "^0.2.0",
- "safe-stable-stringify": "^2.3.1",
- "sonic-boom": "^4.0.1",
- "thread-stream": "^3.0.0"
- },
- "bin": {
- "pino": "bin.js"
- }
- },
"lambdas/authorizer": {
"name": "nhs-notify-supplier-authorizer",
"version": "0.0.1",
@@ -386,26 +357,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
- "lambdas/authorizer/node_modules/pino": {
- "version": "9.14.0",
- "license": "MIT",
- "dependencies": {
- "@pinojs/redact": "^0.4.0",
- "atomic-sleep": "^1.0.0",
- "on-exit-leak-free": "^2.1.0",
- "pino-abstract-transport": "^2.0.0",
- "pino-std-serializers": "^7.0.0",
- "process-warning": "^5.0.0",
- "quick-format-unescaped": "^4.0.3",
- "real-require": "^0.2.0",
- "safe-stable-stringify": "^2.3.1",
- "sonic-boom": "^4.0.1",
- "thread-stream": "^3.0.0"
- },
- "bin": {
- "pino": "bin.js"
- }
- },
"lambdas/letter-updates-transformer": {
"name": "nhs-notify-supplier-api-letter-updates-transformer",
"version": "0.0.1",
@@ -494,6 +445,30 @@
"@esbuild/win32-x64": "0.27.3"
}
},
+ "lambdas/update-letter-queue": {
+ "name": "nhs-notify-supplier-api-update-letter-queue",
+ "version": "0.0.1",
+ "dependencies": {
+ "@aws-sdk/client-dynamodb": "^3.984.0",
+ "@aws-sdk/lib-dynamodb": "^3.984.0",
+ "@aws-sdk/util-dynamodb": "^3.943.0",
+ "@internal/datastore": "*",
+ "aws-embedded-metrics": "^4.2.1",
+ "aws-lambda": "^1.0.6",
+ "esbuild": "0.27.2",
+ "pino": "^10.3.0",
+ "zod": "^4.1.13"
+ },
+ "devDependencies": {
+ "@tsconfig/node22": "^22.0.2",
+ "@types/aws-lambda": "^8.10.148",
+ "@types/jest": "^30.0.0",
+ "jest": "^30.2.0",
+ "jest-mock-extended": "^4.0.0",
+ "ts-jest": "^29.4.0",
+ "typescript": "^5.8.3"
+ }
+ },
"lambdas/upsert-letter": {
"name": "nhs-notify-supplier-api-upsert-letter",
"version": "0.0.1",
@@ -522,26 +497,6 @@
"typescript": "^5.8.3"
}
},
- "lambdas/upsert-letter/node_modules/pino": {
- "version": "9.14.0",
- "license": "MIT",
- "dependencies": {
- "@pinojs/redact": "^0.4.0",
- "atomic-sleep": "^1.0.0",
- "on-exit-leak-free": "^2.1.0",
- "pino-abstract-transport": "^2.0.0",
- "pino-std-serializers": "^7.0.0",
- "process-warning": "^5.0.0",
- "quick-format-unescaped": "^4.0.3",
- "real-require": "^0.2.0",
- "safe-stable-stringify": "^2.3.1",
- "sonic-boom": "^4.0.1",
- "thread-stream": "^3.0.0"
- },
- "bin": {
- "pino": "bin.js"
- }
- },
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "11.9.3",
"license": "MIT",
@@ -764,7 +719,7 @@
}
},
"node_modules/@aws-sdk/client-api-gateway": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
@@ -778,7 +733,7 @@
"@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.985.0",
+ "@aws-sdk/util-endpoints": "3.986.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
"@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
@@ -814,7 +769,7 @@
}
},
"node_modules/@aws-sdk/client-dynamodb": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
@@ -829,7 +784,7 @@
"@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.985.0",
+ "@aws-sdk/util-endpoints": "3.986.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
"@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
@@ -865,7 +820,7 @@
}
},
"node_modules/@aws-sdk/client-kinesis": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
@@ -878,7 +833,7 @@
"@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.985.0",
+ "@aws-sdk/util-endpoints": "3.986.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
"@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
@@ -916,8 +871,61 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/client-lambda": {
+ "version": "3.986.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.7",
+ "@aws-sdk/credential-provider-node": "^3.972.6",
+ "@aws-sdk/middleware-host-header": "^3.972.3",
+ "@aws-sdk/middleware-logger": "^3.972.3",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.3",
+ "@aws-sdk/middleware-user-agent": "^3.972.7",
+ "@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.986.0",
+ "@aws-sdk/util-user-agent-browser": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.5",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/core": "^3.22.1",
+ "@smithy/eventstream-serde-browser": "^4.2.8",
+ "@smithy/eventstream-serde-config-resolver": "^4.3.8",
+ "@smithy/eventstream-serde-node": "^4.2.8",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/hash-node": "^4.2.8",
+ "@smithy/invalid-dependency": "^4.2.8",
+ "@smithy/middleware-content-length": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.13",
+ "@smithy/middleware-retry": "^4.4.30",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/node-http-handler": "^4.4.9",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.11.2",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.29",
+ "@smithy/util-defaults-mode-node": "^4.2.32",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/util-stream": "^4.5.11",
+ "@smithy/util-utf8": "^4.2.0",
+ "@smithy/util-waiter": "^4.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/client-s3": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha1-browser": "5.2.0",
@@ -936,9 +944,9 @@
"@aws-sdk/middleware-ssec": "^3.972.3",
"@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
- "@aws-sdk/signature-v4-multi-region": "3.985.0",
+ "@aws-sdk/signature-v4-multi-region": "3.986.0",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.985.0",
+ "@aws-sdk/util-endpoints": "3.986.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
"@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
@@ -981,7 +989,7 @@
}
},
"node_modules/@aws-sdk/client-sns": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
@@ -994,7 +1002,7 @@
"@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.985.0",
+ "@aws-sdk/util-endpoints": "3.986.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
"@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
@@ -1029,7 +1037,7 @@
}
},
"node_modules/@aws-sdk/client-sqs": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
@@ -1043,7 +1051,7 @@
"@aws-sdk/middleware-user-agent": "^3.972.7",
"@aws-sdk/region-config-resolver": "^3.972.3",
"@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.985.0",
+ "@aws-sdk/util-endpoints": "3.986.0",
"@aws-sdk/util-user-agent-browser": "^3.972.3",
"@aws-sdk/util-user-agent-node": "^3.972.5",
"@smithy/config-resolver": "^4.4.6",
@@ -1125,6 +1133,20 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.985.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/core": {
"version": "3.973.7",
"license": "Apache-2.0",
@@ -1327,11 +1349,11 @@
}
},
"node_modules/@aws-sdk/lib-dynamodb": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.973.7",
- "@aws-sdk/util-dynamodb": "3.985.0",
+ "@aws-sdk/util-dynamodb": "3.986.0",
"@smithy/core": "^3.22.1",
"@smithy/smithy-client": "^4.11.2",
"@smithy/types": "^4.12.0",
@@ -1341,7 +1363,7 @@
"node": ">=20.0.0"
},
"peerDependencies": {
- "@aws-sdk/client-dynamodb": "^3.985.0"
+ "@aws-sdk/client-dynamodb": "^3.986.0"
}
},
"node_modules/@aws-sdk/middleware-bucket-endpoint": {
@@ -1541,6 +1563,20 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.985.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/nested-clients": {
"version": "3.985.0",
"license": "Apache-2.0",
@@ -1588,6 +1624,20 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.985.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/region-config-resolver": {
"version": "3.972.3",
"license": "Apache-2.0",
@@ -1603,10 +1653,10 @@
}
},
"node_modules/@aws-sdk/s3-request-presigner": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/signature-v4-multi-region": "3.985.0",
+ "@aws-sdk/signature-v4-multi-region": "3.986.0",
"@aws-sdk/types": "^3.973.1",
"@aws-sdk/util-format-url": "^3.972.3",
"@smithy/middleware-endpoint": "^4.4.13",
@@ -1620,7 +1670,7 @@
}
},
"node_modules/@aws-sdk/signature-v4-multi-region": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/middleware-sdk-s3": "^3.972.7",
@@ -1672,7 +1722,7 @@
}
},
"node_modules/@aws-sdk/util-dynamodb": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -1681,11 +1731,11 @@
"node": ">=20.0.0"
},
"peerDependencies": {
- "@aws-sdk/client-dynamodb": "^3.985.0"
+ "@aws-sdk/client-dynamodb": "^3.986.0"
}
},
"node_modules/@aws-sdk/util-endpoints": {
- "version": "3.985.0",
+ "version": "3.986.0",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.1",
@@ -2378,17 +2428,6 @@
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
- "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"dev": true,
@@ -3542,7 +3581,7 @@
"license": "MIT"
},
"node_modules/@openapitools/openapi-generator-cli": {
- "version": "2.28.2",
+ "version": "2.28.3",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@@ -3551,7 +3590,7 @@
"@nestjs/common": "11.1.13",
"@nestjs/core": "11.1.13",
"@nuxtjs/opencollective": "0.3.2",
- "axios": "1.13.4",
+ "axios": "1.13.5",
"chalk": "4.1.2",
"commander": "8.3.0",
"compare-versions": "6.1.1",
@@ -3844,62 +3883,6 @@
"node": ">=20"
}
},
- "node_modules/@pact-foundation/pact-core": {
- "version": "17.1.0",
- "cpu": [
- "x64",
- "ia32",
- "arm64"
- ],
- "license": "MIT",
- "os": [
- "darwin",
- "linux",
- "win32"
- ],
- "dependencies": {
- "check-types": "11.2.3",
- "detect-libc": "^2.0.3",
- "node-gyp-build": "^4.6.0",
- "pino": "^10.0.0",
- "pino-pretty": "^13.1.1",
- "underscore": "1.13.7"
- },
- "engines": {
- "node": ">=20"
- },
- "optionalDependencies": {
- "@pact-foundation/pact-core-darwin-arm64": "17.1.0",
- "@pact-foundation/pact-core-darwin-x64": "17.1.0",
- "@pact-foundation/pact-core-linux-arm64-glibc": "17.1.0",
- "@pact-foundation/pact-core-linux-arm64-musl": "17.1.0",
- "@pact-foundation/pact-core-linux-x64-glibc": "17.1.0",
- "@pact-foundation/pact-core-linux-x64-musl": "17.1.0",
- "@pact-foundation/pact-core-windows-x64": "17.1.0"
- }
- },
- "node_modules/@pact-foundation/pact-core-linux-x64-glibc": {
- "version": "17.1.0",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@pact-foundation/pact-core-linux-x64-musl": {
- "version": "17.1.0",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
"node_modules/@pact-foundation/pact/node_modules/@pact-foundation/pact-core": {
"version": "18.0.0",
"cpu": [
@@ -4294,17 +4277,6 @@
"wrap-ansi": "^7.0.0"
}
},
- "node_modules/@redocly/cli/node_modules/dotenv": {
- "version": "16.6.1",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
"node_modules/@redocly/cli/node_modules/glob": {
"version": "7.2.3",
"dev": true,
@@ -4486,17 +4458,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@rollup/pluginutils/node_modules/picomatch": {
- "version": "2.3.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"dev": true,
@@ -5673,6 +5634,32 @@
"eslint": ">=8.40.0"
}
},
+ "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/@tokenizer/inflate": {
"version": "0.4.1",
"dev": true,
@@ -5931,15 +5918,15 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.54.0",
- "@typescript-eslint/type-utils": "8.54.0",
- "@typescript-eslint/utils": "8.54.0",
- "@typescript-eslint/visitor-keys": "8.54.0",
+ "@typescript-eslint/scope-manager": "8.55.0",
+ "@typescript-eslint/type-utils": "8.55.0",
+ "@typescript-eslint/utils": "8.55.0",
+ "@typescript-eslint/visitor-keys": "8.55.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.4.0"
@@ -5952,20 +5939,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.54.0",
+ "@typescript-eslint/parser": "^8.55.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.54.0",
- "@typescript-eslint/types": "8.54.0",
- "@typescript-eslint/typescript-estree": "8.54.0",
- "@typescript-eslint/visitor-keys": "8.54.0",
+ "@typescript-eslint/scope-manager": "8.55.0",
+ "@typescript-eslint/types": "8.55.0",
+ "@typescript-eslint/typescript-estree": "8.55.0",
+ "@typescript-eslint/visitor-keys": "8.55.0",
"debug": "^4.4.3"
},
"engines": {
@@ -5981,12 +5968,12 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.54.0",
- "@typescript-eslint/types": "^8.54.0",
+ "@typescript-eslint/tsconfig-utils": "^8.55.0",
+ "@typescript-eslint/types": "^8.55.0",
"debug": "^4.4.3"
},
"engines": {
@@ -6001,12 +5988,12 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.54.0",
- "@typescript-eslint/visitor-keys": "8.54.0"
+ "@typescript-eslint/types": "8.55.0",
+ "@typescript-eslint/visitor-keys": "8.55.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6017,7 +6004,7 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"engines": {
@@ -6032,13 +6019,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.54.0",
- "@typescript-eslint/typescript-estree": "8.54.0",
- "@typescript-eslint/utils": "8.54.0",
+ "@typescript-eslint/types": "8.55.0",
+ "@typescript-eslint/typescript-estree": "8.55.0",
+ "@typescript-eslint/utils": "8.55.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.4.0"
},
@@ -6055,7 +6042,7 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"engines": {
@@ -6067,14 +6054,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.54.0",
- "@typescript-eslint/tsconfig-utils": "8.54.0",
- "@typescript-eslint/types": "8.54.0",
- "@typescript-eslint/visitor-keys": "8.54.0",
+ "@typescript-eslint/project-service": "8.55.0",
+ "@typescript-eslint/tsconfig-utils": "8.55.0",
+ "@typescript-eslint/types": "8.55.0",
+ "@typescript-eslint/visitor-keys": "8.55.0",
"debug": "^4.4.3",
"minimatch": "^9.0.5",
"semver": "^7.7.3",
@@ -6107,14 +6094,14 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.54.0",
- "@typescript-eslint/types": "8.54.0",
- "@typescript-eslint/typescript-estree": "8.54.0"
+ "@typescript-eslint/scope-manager": "8.55.0",
+ "@typescript-eslint/types": "8.55.0",
+ "@typescript-eslint/typescript-estree": "8.55.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -6129,11 +6116,11 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/types": "8.55.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -6144,6 +6131,17 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"license": "ISC"
@@ -6380,16 +6378,6 @@
"node": ">= 8"
}
},
- "node_modules/anymatch/node_modules/picomatch": {
- "version": "2.3.1",
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/arch": {
"version": "2.2.0",
"funding": [
@@ -7044,8 +7032,6 @@
},
"node_modules/axios": {
"version": "1.13.5",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
- "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
@@ -8798,7 +8784,8 @@
}
},
"node_modules/dotenv": {
- "version": "17.2.4",
+ "version": "16.6.1",
+ "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -9936,11 +9923,11 @@
}
},
"node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
+ "version": "3.4.3",
"dev": true,
"license": "Apache-2.0",
"engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -9970,6 +9957,17 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"dev": true,
@@ -10021,6 +10019,17 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/esprima": {
"version": "4.0.1",
"license": "BSD-2-Clause",
@@ -10375,22 +10384,6 @@
"bser": "2.1.1"
}
},
- "node_modules/fdir": {
- "version": "6.5.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
"node_modules/figures": {
"version": "3.2.0",
"dev": true,
@@ -10812,28 +10805,6 @@
"node": ">= 14"
}
},
- "node_modules/glob": {
- "version": "11.1.0",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "foreground-child": "^3.3.1",
- "jackspeak": "^4.1.1",
- "minimatch": "^10.1.1",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^2.0.0"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "engines": {
- "node": "20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/glob-parent": {
"version": "5.1.2",
"dev": true,
@@ -10849,20 +10820,6 @@
"version": "0.4.1",
"license": "BSD-2-Clause"
},
- "node_modules/glob/node_modules/minimatch": {
- "version": "10.1.2",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/brace-expansion": "^5.0.1"
- },
- "engines": {
- "node": "20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/globals": {
"version": "14.0.0",
"dev": true,
@@ -12701,6 +12658,16 @@
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
+ "node_modules/jest-util/node_modules/picomatch": {
+ "version": "4.0.3",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/jest-validate": {
"version": "30.2.0",
"dev": true,
@@ -13325,35 +13292,15 @@
"node": ">= 0.6"
}
},
- "node_modules/micromatch": {
- "version": "4.0.8",
- "license": "MIT",
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/micromatch/node_modules/picomatch": {
- "version": "2.3.1",
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/mime": {
- "version": "1.6.0",
+ "node_modules/micromatch": {
+ "version": "4.0.8",
"license": "MIT",
- "bin": {
- "mime": "cli.js"
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
},
"engines": {
- "node": ">=4"
+ "node": ">=8.6"
}
},
"node_modules/mime-db": {
@@ -13589,6 +13536,10 @@
"resolved": "tests",
"link": true
},
+ "node_modules/nhs-notify-supplier-api-update-letter-queue": {
+ "resolved": "lambdas/update-letter-queue",
+ "link": true
+ },
"node_modules/nhs-notify-supplier-api-upsert-letter": {
"resolved": "lambdas/upsert-letter",
"link": true
@@ -14347,17 +14298,17 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.3",
+ "version": "2.3.1",
"license": "MIT",
"engines": {
- "node": ">=12"
+ "node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pino": {
- "version": "10.3.0",
+ "version": "10.3.1",
"license": "MIT",
"dependencies": {
"@pinojs/redact": "^0.4.0",
@@ -14377,7 +14328,7 @@
}
},
"node_modules/pino-abstract-transport": {
- "version": "2.0.0",
+ "version": "3.0.0",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
@@ -14416,13 +14367,6 @@
"node": "*"
}
},
- "node_modules/pino-pretty/node_modules/pino-abstract-transport": {
- "version": "3.0.0",
- "license": "MIT",
- "dependencies": {
- "split2": "^4.0.0"
- }
- },
"node_modules/pino-pretty/node_modules/strip-json-comments": {
"version": "5.0.3",
"license": "MIT",
@@ -14437,23 +14381,6 @@
"version": "7.1.0",
"license": "MIT"
},
- "node_modules/pino/node_modules/pino-abstract-transport": {
- "version": "3.0.0",
- "license": "MIT",
- "dependencies": {
- "split2": "^4.0.0"
- }
- },
- "node_modules/pino/node_modules/thread-stream": {
- "version": "4.0.0",
- "license": "MIT",
- "dependencies": {
- "real-require": "^0.2.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
"node_modules/pirates": {
"version": "4.0.7",
"license": "MIT",
@@ -15020,17 +14947,6 @@
"node": ">=8.10.0"
}
},
- "node_modules/readdirp/node_modules/picomatch": {
- "version": "2.3.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/real-require": {
"version": "0.2.0",
"license": "MIT",
@@ -16628,10 +16544,13 @@
"license": "MIT"
},
"node_modules/thread-stream": {
- "version": "3.1.0",
+ "version": "4.0.0",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=20"
}
},
"node_modules/through": {
@@ -16654,6 +16573,33 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/tldts": {
"version": "6.1.86",
"dev": true,
@@ -17108,14 +17054,14 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.54.0",
+ "version": "8.55.0",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.54.0",
- "@typescript-eslint/parser": "8.54.0",
- "@typescript-eslint/typescript-estree": "8.54.0",
- "@typescript-eslint/utils": "8.54.0"
+ "@typescript-eslint/eslint-plugin": "8.55.0",
+ "@typescript-eslint/parser": "8.55.0",
+ "@typescript-eslint/typescript-estree": "8.55.0",
+ "@typescript-eslint/utils": "8.55.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -17192,6 +17138,10 @@
"node": ">=20.18.1"
}
},
+ "node_modules/undici-types": {
+ "version": "7.21.0",
+ "license": "MIT"
+ },
"node_modules/universalify": {
"version": "2.0.1",
"dev": true,
@@ -18018,26 +17968,6 @@
"@esbuild/win32-x64": "0.25.12"
}
},
- "scripts/utilities/letter-test-data/node_modules/pino": {
- "version": "9.14.0",
- "license": "MIT",
- "dependencies": {
- "@pinojs/redact": "^0.4.0",
- "atomic-sleep": "^1.0.0",
- "on-exit-leak-free": "^2.1.0",
- "pino-abstract-transport": "^2.0.0",
- "pino-std-serializers": "^7.0.0",
- "process-warning": "^5.0.0",
- "quick-format-unescaped": "^4.0.3",
- "real-require": "^0.2.0",
- "safe-stable-stringify": "^2.3.1",
- "sonic-boom": "^4.0.1",
- "thread-stream": "^3.0.0"
- },
- "bin": {
- "pino": "bin.js"
- }
- },
"scripts/utilities/supplier-data": {
"name": "nhs-notify-supplier-api-suppliers-data-utility",
"version": "0.0.1",
@@ -18110,26 +18040,6 @@
"@esbuild/win32-x64": "0.25.12"
}
},
- "scripts/utilities/supplier-data/node_modules/pino": {
- "version": "9.14.0",
- "license": "MIT",
- "dependencies": {
- "@pinojs/redact": "^0.4.0",
- "atomic-sleep": "^1.0.0",
- "on-exit-leak-free": "^2.1.0",
- "pino-abstract-transport": "^2.0.0",
- "pino-std-serializers": "^7.0.0",
- "process-warning": "^5.0.0",
- "quick-format-unescaped": "^4.0.3",
- "real-require": "^0.2.0",
- "safe-stable-stringify": "^2.3.1",
- "sonic-boom": "^4.0.1",
- "thread-stream": "^3.0.0"
- },
- "bin": {
- "pino": "bin.js"
- }
- },
"tests": {
"name": "nhs-notify-supplier-api-tests",
"version": "0.0.1",
@@ -18189,84 +18099,107 @@
"typescript": "^5.9.3"
}
},
- "tests/node_modules/@aws-sdk/client-lambda": {
- "version": "3.986.0",
- "license": "Apache-2.0",
+ "tests/node_modules/@pact-foundation/pact-core": {
+ "version": "17.1.0",
+ "cpu": [
+ "x64",
+ "ia32",
+ "arm64"
+ ],
+ "license": "MIT",
+ "os": [
+ "darwin",
+ "linux",
+ "win32"
+ ],
"dependencies": {
- "@aws-crypto/sha256-browser": "5.2.0",
- "@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.7",
- "@aws-sdk/credential-provider-node": "^3.972.6",
- "@aws-sdk/middleware-host-header": "^3.972.3",
- "@aws-sdk/middleware-logger": "^3.972.3",
- "@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.7",
- "@aws-sdk/region-config-resolver": "^3.972.3",
- "@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.986.0",
- "@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.5",
- "@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.1",
- "@smithy/eventstream-serde-browser": "^4.2.8",
- "@smithy/eventstream-serde-config-resolver": "^4.3.8",
- "@smithy/eventstream-serde-node": "^4.2.8",
- "@smithy/fetch-http-handler": "^5.3.9",
- "@smithy/hash-node": "^4.2.8",
- "@smithy/invalid-dependency": "^4.2.8",
- "@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.13",
- "@smithy/middleware-retry": "^4.4.30",
- "@smithy/middleware-serde": "^4.2.9",
- "@smithy/middleware-stack": "^4.2.8",
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.9",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.2",
- "@smithy/types": "^4.12.0",
- "@smithy/url-parser": "^4.2.8",
- "@smithy/util-base64": "^4.3.0",
- "@smithy/util-body-length-browser": "^4.2.0",
- "@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.29",
- "@smithy/util-defaults-mode-node": "^4.2.32",
- "@smithy/util-endpoints": "^3.2.8",
- "@smithy/util-middleware": "^4.2.8",
- "@smithy/util-retry": "^4.2.8",
- "@smithy/util-stream": "^4.5.11",
- "@smithy/util-utf8": "^4.2.0",
- "@smithy/util-waiter": "^4.2.8",
- "tslib": "^2.6.2"
+ "check-types": "11.2.3",
+ "detect-libc": "^2.0.3",
+ "node-gyp-build": "^4.6.0",
+ "pino": "^10.0.0",
+ "pino-pretty": "^13.1.1",
+ "underscore": "1.13.7"
},
"engines": {
- "node": ">=20.0.0"
+ "node": ">=20"
+ },
+ "optionalDependencies": {
+ "@pact-foundation/pact-core-darwin-arm64": "17.1.0",
+ "@pact-foundation/pact-core-darwin-x64": "17.1.0",
+ "@pact-foundation/pact-core-linux-arm64-glibc": "17.1.0",
+ "@pact-foundation/pact-core-linux-arm64-musl": "17.1.0",
+ "@pact-foundation/pact-core-linux-x64-glibc": "17.1.0",
+ "@pact-foundation/pact-core-linux-x64-musl": "17.1.0",
+ "@pact-foundation/pact-core-windows-x64": "17.1.0"
}
},
- "tests/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.986.0",
- "license": "Apache-2.0",
+ "tests/node_modules/@pact-foundation/pact-core-linux-x64-glibc": {
+ "version": "17.1.0",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "tests/node_modules/@pact-foundation/pact-core-linux-x64-musl": {
+ "version": "17.1.0",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "tests/node_modules/dotenv": {
+ "version": "17.2.4",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "tests/node_modules/glob": {
+ "version": "11.1.0",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/types": "^4.12.0",
- "@smithy/url-parser": "^4.2.8",
- "@smithy/util-endpoints": "^3.2.8",
- "tslib": "^2.6.2"
+ "foreground-child": "^3.3.1",
+ "jackspeak": "^4.1.1",
+ "minimatch": "^10.1.1",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^2.0.0"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
},
"engines": {
- "node": ">=20.0.0"
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "tests/node_modules/@types/node": {
- "version": "24.10.12",
+ "tests/node_modules/minimatch": {
+ "version": "10.1.2",
"dev": true,
- "license": "MIT",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "undici-types": "~7.16.0"
+ "@isaacs/brace-expansion": "^5.0.1"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
- },
- "tests/node_modules/undici-types": {
- "version": "7.16.0",
- "license": "MIT"
}
}
}