From c96951d739673464eb560848a897e8704528322d Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Tue, 10 Feb 2026 15:24:50 +0000 Subject: [PATCH 1/3] CCM-12934 Add LetterQueue table --- .../components/api/ddb_table_letter_queue.tf | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 infrastructure/terraform/components/api/ddb_table_letter_queue.tf 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 00000000..83cf1f24 --- /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" + } + ) +} From 4dd908f28166f2525e847b35c421193db450368e Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Wed, 11 Feb 2026 08:57:28 +0000 Subject: [PATCH 2/3] CCM-12934 Add repository for new table --- .vscode/settings.json | 3 +- internal/datastore/src/__test__/db.ts | 58 +++++++--- .../__test__/letter-queue-repository.test.ts | 104 ++++++++++++++++++ internal/datastore/src/config.ts | 2 + .../datastore/src/letter-queue-repository.ts | 72 ++++++++++++ internal/datastore/src/types.ts | 17 +++ 6 files changed, 238 insertions(+), 18 deletions(-) create mode 100644 internal/datastore/src/__test__/letter-queue-repository.test.ts create mode 100644 internal/datastore/src/letter-queue-repository.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index d7c02400..3f98619f 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/internal/datastore/src/__test__/db.ts b/internal/datastore/src/__test__/db.ts index 1d364b9f..f382add6 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 00000000..b4ec6b64 --- /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 6440942e..4066101a 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/letter-queue-repository.ts b/internal/datastore/src/letter-queue-repository.ts new file mode 100644 index 00000000..319e47ab --- /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 a0b9f719..12925565 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(), From 065d0491abe9ba58f13850dfe85d30a4482e02dc Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Fri, 13 Feb 2026 15:27:07 +0000 Subject: [PATCH 3/3] Add update-letter-queue lambda --- .../terraform/components/api/README.md | 1 + ...vent_source_mapping_update_letter_queue.tf | 11 + .../api/module_lambda_update_letter_queue.tf | 70 ++ internal/datastore/src/index.ts | 1 + lambdas/update-letter-queue/.eslintignore | 3 + lambdas/update-letter-queue/.gitignore | 3 + lambdas/update-letter-queue/jest.config.ts | 65 ++ lambdas/update-letter-queue/package.json | 32 + .../src/__tests__/types.ts | 12 + .../src/__tests__/update-letter-queue.test.ts | 249 +++++ lambdas/update-letter-queue/src/deps.ts | 32 + lambdas/update-letter-queue/src/env.ts | 10 + lambdas/update-letter-queue/src/index.ts | 7 + .../src/update-letter-queue.ts | 147 +++ lambdas/update-letter-queue/tsconfig.json | 11 + package-lock.json | 967 ++++++++---------- 16 files changed, 1104 insertions(+), 517 deletions(-) create mode 100644 infrastructure/terraform/components/api/event_source_mapping_update_letter_queue.tf create mode 100644 infrastructure/terraform/components/api/module_lambda_update_letter_queue.tf create mode 100644 lambdas/update-letter-queue/.eslintignore create mode 100644 lambdas/update-letter-queue/.gitignore create mode 100644 lambdas/update-letter-queue/jest.config.ts create mode 100644 lambdas/update-letter-queue/package.json create mode 100644 lambdas/update-letter-queue/src/__tests__/types.ts create mode 100644 lambdas/update-letter-queue/src/__tests__/update-letter-queue.test.ts create mode 100644 lambdas/update-letter-queue/src/deps.ts create mode 100644 lambdas/update-letter-queue/src/env.ts create mode 100644 lambdas/update-letter-queue/src/index.ts create mode 100644 lambdas/update-letter-queue/src/update-letter-queue.ts create mode 100644 lambdas/update-letter-queue/tsconfig.json diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md index 75faa887..8b5b020c 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/event_source_mapping_update_letter_queue.tf b/infrastructure/terraform/components/api/event_source_mapping_update_letter_queue.tf new file mode 100644 index 00000000..9546f4ef --- /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 00000000..f25bf7a3 --- /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/index.ts b/internal/datastore/src/index.ts index 53d52cd6..850d5039 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/lambdas/update-letter-queue/.eslintignore b/lambdas/update-letter-queue/.eslintignore new file mode 100644 index 00000000..6d6ccf00 --- /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 00000000..6d6ccf00 --- /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 00000000..e173d5e3 --- /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 00000000..6ae22059 --- /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 00000000..292af5e1 --- /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 00000000..c28c1239 --- /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 00000000..e1fd14f3 --- /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 00000000..1902cea9 --- /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 00000000..1278c8ee --- /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 00000000..65e2d0dd --- /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 00000000..f3fa0970 --- /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 a63b758e..ae19fa77 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" } } }