diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ccc86de90..e1a4460731 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,7 @@ jobs: ENABLE_BACKUP: "True" ENABLE_NOTIFICATIONS_INTERNAL: true ENABLE_NOTIFICATIONS_EXTERNAL: false + ENABLE_POST_DATED_NOTIFICATIONS: true ENABLED_SYSTEMS: "Internal Test System" BLOCKED_SITE_ODS_CODES: "B3J1Z" NOTIFY_ROUTING_PLAN_ID: "e57fe5cc-0567-4854-abe2-b7dd9014a50c" @@ -126,6 +127,7 @@ jobs: ENABLE_BACKUP: "False" ENABLE_NOTIFICATIONS_INTERNAL: false ENABLE_NOTIFICATIONS_EXTERNAL: false + ENABLE_POST_DATED_NOTIFICATIONS: true ENABLED_SYSTEMS: "Internal Test System" BLOCKED_SITE_ODS_CODES: "B3J1Z" NOTIFY_ROUTING_PLAN_ID: "e57fe5cc-0567-4854-abe2-b7dd9014a50c" @@ -163,6 +165,7 @@ jobs: ENABLE_BACKUP: "False" ENABLE_NOTIFICATIONS_INTERNAL: false ENABLE_NOTIFICATIONS_EXTERNAL: false + ENABLE_POST_DATED_NOTIFICATIONS: true ENABLED_SYSTEMS: "Internal Test System" BLOCKED_SITE_ODS_CODES: "B3J1Z" NOTIFY_ROUTING_PLAN_ID: "e57fe5cc-0567-4854-abe2-b7dd9014a50c" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ed8556c9bd..99071d49d3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -119,6 +119,7 @@ jobs: ENABLE_BACKUP: "False" ENABLE_NOTIFICATIONS_INTERNAL: true ENABLE_NOTIFICATIONS_EXTERNAL: false + ENABLE_POST_DATED_NOTIFICATIONS: true ENABLED_SYSTEMS: "Internal Test System" BLOCKED_SITE_ODS_CODES: "XXXXX" # Workaround empty string handling NOTIFY_ROUTING_PLAN_ID: "e57fe5cc-0567-4854-abe2-b7dd9014a50c" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a7108360f..71b2804ec4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,6 +84,7 @@ jobs: ENABLE_BACKUP: "True" ENABLE_NOTIFICATIONS_INTERNAL: false ENABLE_NOTIFICATIONS_EXTERNAL: false + ENABLE_POST_DATED_NOTIFICATIONS: true ENABLED_SYSTEMS: "Internal Test System" BLOCKED_SITE_ODS_CODES: "XXXXX" # Workaround empty string handling NOTIFY_ROUTING_PLAN_ID: "e57fe5cc-0567-4854-abe2-b7dd9014a50c" @@ -223,6 +224,7 @@ jobs: ENABLE_BACKUP: "False" ENABLE_NOTIFICATIONS_INTERNAL: false ENABLE_NOTIFICATIONS_EXTERNAL: false + ENABLE_POST_DATED_NOTIFICATIONS: true ENABLED_SYSTEMS: "Internal Test System, Apotec Ltd - Apotec CRM - Production, CrxPatientApp, nhsPrescriptionApp, Titan PSU Prod" BLOCKED_SITE_ODS_CODES: "B3J1Z" NOTIFY_ROUTING_PLAN_ID: "e57fe5cc-0567-4854-abe2-b7dd9014a50c" @@ -266,6 +268,7 @@ jobs: ENABLE_BACKUP: "True" ENABLE_NOTIFICATIONS_INTERNAL: true ENABLE_NOTIFICATIONS_EXTERNAL: true + ENABLE_POST_DATED_NOTIFICATIONS: true ENABLED_SYSTEMS: "Internal Test System" # Workaround empty string handling BLOCKED_SITE_ODS_CODES: "XXXXX" # Workaround empty string handling NOTIFY_ROUTING_PLAN_ID: "e57fe5cc-0567-4854-abe2-b7dd9014a50c" diff --git a/.github/workflows/run_release_code_and_api.yml b/.github/workflows/run_release_code_and_api.yml index 756e5b4db6..0ee15ac0e1 100644 --- a/.github/workflows/run_release_code_and_api.yml +++ b/.github/workflows/run_release_code_and_api.yml @@ -84,6 +84,10 @@ on: required: false type: boolean default: false + ENABLE_POST_DATED_NOTIFICATIONS: + required: false + type: boolean + default: false ENABLED_SYSTEMS: required: true type: string @@ -215,6 +219,7 @@ jobs: ENABLE_BACKUP: ${{ inputs.ENABLE_BACKUP }} ENABLE_NOTIFICATIONS_INTERNAL: ${{ inputs.ENABLE_NOTIFICATIONS_INTERNAL }} ENABLE_NOTIFICATIONS_EXTERNAL: ${{ inputs.ENABLE_NOTIFICATIONS_EXTERNAL }} + ENABLE_POST_DATED_NOTIFICATIONS: ${{ inputs.ENABLE_POST_DATED_NOTIFICATIONS }} REQUIRE_APPLICATION_NAME: ${{ inputs.REQUIRE_APPLICATION_NAME }} ENABLED_SITE_ODS_CODES: ${{ steps.read.outputs.ods_csv }} ENABLED_SYSTEMS: ${{ inputs.ENABLED_SYSTEMS }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb715fc821..1fae54bc68 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -86,7 +86,7 @@ repos: files: ^packages\/checkPrescriptionStatusUpdates types_or: [ts, tsx, javascript, jsx, json] pass_filenames: false - + - id: lint-nhsNotifyLambda name: Lint nhsNotifyLambda entry: npm @@ -97,6 +97,16 @@ repos: types_or: [ts, tsx, javascript, jsx, json] pass_filenames: false + - id: lint-postDatedLambda + name: List Post-dated handling lambda + entry: npm + args: + ["run", "--prefix=packages/postDatedLambda", "lint"] + language: system + files: ^packages\/postDatedLambda + types_or: [ts, tsx, javascript, jsx, json] + pass_filenames: false + - id: lint-commonTesting name: Lint common/testing entry: npm diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 755f378459..3ff45dc79f 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -5,5 +5,3 @@ vulnerabilities: - id: CVE-2026-25547 statement: isaacs/brace-expansion vulnerability accepted as risk - dependency of aws-sdk/client-dynamodb expired_at: 2026-03-01 - - diff --git a/.vscode/eps-prescription-status-update-api.code-workspace b/.vscode/eps-prescription-status-update-api.code-workspace index 0653f825d2..06664a3e91 100644 --- a/.vscode/eps-prescription-status-update-api.code-workspace +++ b/.vscode/eps-prescription-status-update-api.code-workspace @@ -32,6 +32,10 @@ "name": "packages/nhsNotifyLambda", "path": "../packages/nhsNotifyLambda" }, + { + "name": "packages/postDatedLambda", + "path": "../packages/postDatedLambda" + }, { "name": "packages/nhsNotifyUpdateCallback", "path": "../packages/nhsNotifyUpdateCallback" diff --git a/Makefile b/Makefile index ea64ca0d35..8acae25488 100644 --- a/Makefile +++ b/Makefile @@ -104,7 +104,7 @@ sam-list-resources: guard-AWS_DEFAULT_PROFILE guard-stack_name sam-list-outputs: guard-AWS_DEFAULT_PROFILE guard-stack_name sam list stack-outputs --stack-name $$stack_name -sam-validate: +sam-validate: sam validate --template-file SAMtemplates/main_template.yaml --region eu-west-2 sam validate --template-file SAMtemplates/apis/main.yaml --region eu-west-2 sam validate --template-file SAMtemplates/apis/api_resources.yaml --region eu-west-2 @@ -168,6 +168,7 @@ lint-node: compile-node npm run lint --workspace packages/cpsuLambda npm run lint --workspace packages/checkPrescriptionStatusUpdates npm run lint --workspace packages/nhsNotifyLambda + npm run lint --workspace packages/postDatedLambda npm run lint --workspace packages/nhsNotifyUpdateCallback npm run lint --workspace packages/common/testing npm run lint --workspace packages/common/middyErrorHandler @@ -200,6 +201,7 @@ test: compile npm run test --workspace packages/cpsuLambda npm run test --workspace packages/checkPrescriptionStatusUpdates npm run test --workspace packages/nhsNotifyLambda + npm run test --workspace packages/postDatedLambda npm run test --workspace packages/nhsNotifyUpdateCallback npm run test --workspace packages/common/middyErrorHandler npm run test --workspace packages/psuRestoreValidationLambda @@ -220,6 +222,8 @@ clean: rm -rf packages/cpsuLambda/lib rm -rf packages/nhsNotifyLambda/coverage rm -rf packages/nhsNotifyLambda/lib + rm -rf packages/postDatedLambda/coverage + rm -rf packages/postDatedLambda/lib rm -rf packages/nhsNotifyUpdateCallback/coverage rm -rf packages/nhsNotifyUpdateCallback/lib rm -rf packages/checkPrescriptionStatusUpdates/lib diff --git a/README.md b/README.md index 9f911f2d16..570472976d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # EPS Prescription Status Update API -![Build](https://github.com/NHSDigital/eps-prescription-status-update-api/actions/workflows/ci.yml/badge.svg?branch=main) +![Build](https://github.com/NHSDigital/eps-prescription-status-update-api/actions/workflows/ci.yml/badge.svg?branch=main) ![Release](https://github.com/NHSDigital/eps-prescription-status-update-api/actions/workflows/release.yml/badge.svg?branch=main) ## Versions and deployments -Version release history can be found ot https://github.com/NHSDigital/eps-prescription-status-update-api/releases. -We use eslint convention for commit messages for commits to main branch. Descriptions for the types of changes in a release can be found in the [contributing guidelines](./CONTRIBUTING.md) +Version release history can be found ot https://github.com/NHSDigital/eps-prescription-status-update-api/releases. +We use eslint convention for commit messages for commits to main branch. Descriptions for the types of changes in a release can be found in the [contributing guidelines](./CONTRIBUTING.md) Deployment history can be found at https://nhsdigital.github.io/eps-prescription-status-update-api/ ## Introduction @@ -20,6 +20,7 @@ This is the AWS layer that provides an API for EPS Prescription Status Update. - `packages/capabilityStatement` Returns a static capability statement. - `packages/cpsuLambda` Handles updating prescription status using a custom format. - `packages/nhsNotifyLambda` Handles sending prescription notifications to the NHS Notify service. +- `packages/postDatedLambda` Handles business logic for post-dated prescriptions getting notifications \[deprecated\]. - `packages/nhsNotifyUpdateCallback` Handles receiving notification updates from the NHS Notify service. - `packages/checkPrescriptionStatusUpdates` Validates and retrieves prescription status update data. - `packages/gsul` Expose data owned by PSU but needed by [PfP](https://github.com/NHSDigital/prescriptionsforpatients) @@ -56,9 +57,9 @@ The contents of this repository are protected by Crown Copyright (C). ## Development -It is recommended that you use visual studio code and a devcontainer as this will install all necessary components and correct versions of tools and languages. -See https://code.visualstudio.com/docs/devcontainers/containers for details on how to set this up on your host machine. -There is also a workspace file in .vscode that should be opened once you have started the devcontainer. The workspace file can also be opened outside of a devcontainer if you wish. +It is recommended that you use visual studio code and a devcontainer as this will install all necessary components and correct versions of tools and languages. +See https://code.visualstudio.com/docs/devcontainers/containers for details on how to set this up on your host machine. +There is also a workspace file in .vscode that should be opened once you have started the devcontainer. The workspace file can also be opened outside of a devcontainer if you wish. The project uses [SAM](https://aws.amazon.com/serverless/sam/) to develop and deploy the APIs and associated resources. All commits must be made using [signed commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). @@ -239,8 +240,8 @@ Workflows are in the `.github/workflows` folder: ### Github pages -Github pages is used to display deployment information. The source for github pages is in the gh-pages branch. -As part of the ci and release workflows, the release tag (either the short commit SHA or release tag) is appended to \_data/{environment}\_deployments.csv so we have a history of releases and replaced in \_data/{environment}\_latest.csv so we now what the latest released version is. +Github pages is used to display deployment information. The source for github pages is in the gh-pages branch. +As part of the ci and release workflows, the release tag (either the short commit SHA or release tag) is appended to \_data/{environment}\_deployments.csv so we have a history of releases and replaced in \_data/{environment}\_latest.csv so we now what the latest released version is. There are different makefile targets in this branch. These are - `run-jekyll` - runs the site locally so changes can be previewed during development diff --git a/SAMtemplates/functions/main.yaml b/SAMtemplates/functions/main.yaml index 9ebfb31b6b..2c8868377a 100644 --- a/SAMtemplates/functions/main.yaml +++ b/SAMtemplates/functions/main.yaml @@ -33,6 +33,10 @@ Parameters: Type: String Default: none + PostDatedNotificationsSQSQueueUrl: + Type: String + Default: none + SQSSaltSecret: Type: AWS::SecretsManager::Secret::Name @@ -57,6 +61,10 @@ Parameters: EnableNotificationsInternalParam: Type: AWS::SSM::Parameter::Name + EnablePostDatedNotifications: + Type: AWS::SSM::Parameter::Name + Default: "false" + RequireApplicationName: Type: String Default: false @@ -121,6 +129,8 @@ Resources: Variables: TABLE_NAME: !Ref PrescriptionStatusUpdatesTableName NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL: !Ref NHSNotifyPrescriptionsSQSQueueUrl + POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL: !Ref PostDatedNotificationsSQSQueueUrl + ENABLE_POST_DATED_NOTIFICATIONS: !Ref EnablePostDatedNotifications SQS_SALT: !Ref SQSSaltSecret ENABLED_SITE_ODS_CODES_PARAM: !Ref EnabledSiteODSCodesParam ENABLED_SYSTEMS_PARAM: !Ref EnabledSystemsParam @@ -165,6 +175,7 @@ Resources: - Fn::ImportValue: !Sub ${StackName}:tables:UsePrescriptionStatusUpdatesKMSKeyPolicyArn - Fn::ImportValue: !Sub ${StackName}-UseNotificationSQSQueueKMSKeyPolicyArn - Fn::ImportValue: !Sub ${StackName}-WriteNHSNotifyPrescriptionsSQSQueuePolicyArn + - Fn::ImportValue: !Sub ${StackName}-WritePostDatedNotificationsSQSQueuePolicyArn - Fn::ImportValue: !Sub ${StackName}-GetSQSSaltSecretPolicy - Fn::ImportValue: !Sub ${StackName}-GetNotificationsParameterPolicy LogRetentionInDays: !Ref LogRetentionInDays @@ -489,6 +500,98 @@ Resources: - Fn::ImportValue: !Sub ${StackName}-GetPSUSecretPolicy - Fn::ImportValue: !Sub ${StackName}-UsePSUSecretsKMSKeyPolicyArn + ## Post-dated lambda definitions + PostDatedNotifyLambdaScheduleEventRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: + Service: + - scheduler.amazonaws.com + Action: + - sts:AssumeRole + ManagedPolicyArns: + - !Ref PostDatedNotifyLambdaScheduleEventRolePolicy + + PostDatedNotifyLambdaScheduleEventRolePolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: + - !GetAtt PostDatedNotifyLambda.Arn + + PostDatedNotifyLambda: + Type: AWS::Serverless::Function + Properties: + FunctionName: !Sub ${StackName}-postDatedNotifyLambda + Timeout: 900 + CodeUri: ../../packages/ + Handler: main.handler + Role: !GetAtt PostDatedNotifyLambdaResources.Outputs.LambdaRoleArn + Environment: + Variables: + LOG_LEVEL: !Ref LogLevel + NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL: !Ref NHSNotifyPrescriptionsSQSQueueUrl + POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL: !Ref PostDatedNotificationsSQSQueueUrl + TABLE_NAME: !Ref PrescriptionStatusUpdatesTableName + Events: + ScheduleEvent: + Type: ScheduleV2 + Properties: + Name: !Sub ${StackName}-PostDatedNotifySchedule + ScheduleExpression: "rate(15 minutes)" + RoleArn: !GetAtt PostDatedNotifyLambdaScheduleEventRole.Arn + Metadata: + BuildMethod: esbuild + guard: + SuppressedRules: + - LAMBDA_DLQ_CHECK + - LAMBDA_INSIDE_VPC + - LAMBDA_CONCURRENCY_CHECK + BuildProperties: + Minify: true + Target: es2020 + Sourcemap: true + packages: bundle + # set tsconfig path to whatever you actually have for this lambda package + tsconfig: postDatedLambda/tsconfig.json + EntryPoints: + - postDatedLambda/src/main.ts + + PostDatedNotifyLambdaResources: + Type: AWS::Serverless::Application + Properties: + Location: lambda_resources.yaml + Parameters: + StackName: !Ref StackName + LambdaName: !Sub ${StackName}-postDatedNotifyLambda + LambdaArn: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${StackName}-postDatedNotifyLambda + LogRetentionInDays: !Ref LogRetentionInDays + CloudWatchKMSKeyId: !ImportValue account-resources:CloudwatchLogsKmsKeyArn + EnableSplunk: !Ref EnableSplunk + SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole + SplunkDeliveryStreamArn: !ImportValue lambda-resources:SplunkDeliveryStream + IncludeAdditionalPolicies: true + AdditionalPolicies: !Join + - "," + - - Fn::ImportValue: !Sub ${StackName}-WriteNHSNotifyPrescriptionsSQSQueuePolicyArn + - Fn::ImportValue: !Sub ${StackName}-ReadNHSNotifyPrescriptionsSQSQueuePolicyArn + - Fn::ImportValue: !Sub ${StackName}-UseNotificationSQSQueueKMSKeyPolicyArn + - Fn::ImportValue: !Sub ${StackName}-WritePostDatedNotificationsSQSQueuePolicyArn + - Fn::ImportValue: !Sub ${StackName}-ReadPostDatedNotificationsSQSQueuePolicyArn + - Fn::ImportValue: !Sub ${StackName}:tables:${PrescriptionStatusUpdatesTableName}:TableWritePolicyArn + - Fn::ImportValue: !Sub ${StackName}:tables:${PrescriptionStatusUpdatesTableName}:TableReadPolicyArn + - Fn::ImportValue: !Sub ${StackName}:tables:UsePrescriptionStatusUpdatesKMSKeyPolicyArn + ## End of post-dated lambda bits + NHSNotifyUpdateCallback: Type: AWS::Serverless::Function Properties: @@ -719,3 +822,12 @@ Outputs: NHSNotifyUpdateCallbackFunctionArn: Description: The function ARN of the NHSNotifyUpdateCallback lambda Value: !GetAtt NHSNotifyUpdateCallback.Arn + + # Post-dated lambda outputs + PostDatedNotifyLambdaFunctionName: + Description: The function name of the postDatedNotifyLambda + Value: !Ref PostDatedNotifyLambda + + PostDatedNotifyLambdaFunctionArn: + Description: The function ARN of the postDatedNotifyLambda + Value: !GetAtt PostDatedNotifyLambda.Arn diff --git a/SAMtemplates/main_template.yaml b/SAMtemplates/main_template.yaml index 9d4bd42057..08c15fcf7a 100644 --- a/SAMtemplates/main_template.yaml +++ b/SAMtemplates/main_template.yaml @@ -126,6 +126,10 @@ Parameters: Type: String Default: " " + EnablePostDatedNotifications: + Type: String + Default: "false" + NotifyRoutingPlanIDValue: Type: String Default: " " @@ -235,6 +239,8 @@ Resources: PrescriptionStatusUpdatesTableName: !GetAtt Tables.Outputs.PrescriptionStatusUpdatesTableName PrescriptionNotificationStatesTableName: !GetAtt Tables.Outputs.PrescriptionNotificationStatesTableName NHSNotifyPrescriptionsSQSQueueUrl: !GetAtt Messaging.Outputs.NHSNotifyPrescriptionsSQSQueueUrl + PostDatedNotificationsSQSQueueUrl: !GetAtt Messaging.Outputs.PostDatedNotificationsSQSQueueUrl + EnablePostDatedNotifications: !Ref EnablePostDatedNotifications SQSSaltSecret: !GetAtt Secrets.Outputs.SQSSaltSecret EnabledSiteODSCodesParam: !GetAtt Parameters.Outputs.EnabledSiteODSCodesParameterName EnabledSystemsParam: !GetAtt Parameters.Outputs.EnabledSystemsParameterName diff --git a/SAMtemplates/messaging/main.yaml b/SAMtemplates/messaging/main.yaml index dea4007371..c15836cd17 100644 --- a/SAMtemplates/messaging/main.yaml +++ b/SAMtemplates/messaging/main.yaml @@ -102,6 +102,65 @@ Resources: - kms:Decrypt Resource: !GetAtt NHSNotifyPrescriptionsSQSQueue.Arn + ## Post-dated SQS queue starts here + PostDatedNotificationsSQSQueue: + Type: AWS::SQS::Queue + Properties: + QueueName: !Sub ${StackName}-PostDatedNotifications.fifo + FifoQueue: true + ContentBasedDeduplication: false + KmsMasterKeyId: !Ref NotificationSQSQueueKMSKeyAlias + MessageRetentionPeriod: 86400 # 1 day in seconds + RedrivePolicy: + deadLetterTargetArn: !GetAtt PostDatedNotificationsDeadLetterQueue.Arn + maxReceiveCount: 166 # 999 hours / 6 hours + VisibilityTimeout: 300 + + PostDatedNotificationsDeadLetterQueue: + Type: AWS::SQS::Queue + Properties: + QueueName: !Sub ${StackName}-PostDatedNotificationsDeadLetter.fifo + FifoQueue: true + ContentBasedDeduplication: false + KmsMasterKeyId: !Ref NotificationSQSQueueKMSKeyAlias + MessageRetentionPeriod: 604800 # 1 week in seconds + VisibilityTimeout: 300 + + ReadPostDatedNotificationsSQSQueuePolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - sqs:ReceiveMessage + - sqs:DeleteMessage + - sqs:ChangeMessageVisibility + - sqs:GetQueueAttributes + - sqs:GetQueueUrl + - kms:GenerateDataKey + - kms:Decrypt + Resource: !GetAtt PostDatedNotificationsSQSQueue.Arn + + WritePostDatedNotificationsSQSQueuePolicy: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: !Sub ${StackName}-PostDatedNotificationsSendMessagePolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - sqs:SendMessage + - sqs:SendMessageBatch + - sqs:DeleteMessage + - sqs:GetQueueUrl + - kms:GenerateDataKey + - kms:Decrypt + Resource: !GetAtt PostDatedNotificationsSQSQueue.Arn + # End of post-dated stuff + Outputs: NHSNotifyPrescriptionsSQSQueueUrl: Description: The URL of the NHS Notify Prescriptions SQS Queue @@ -138,3 +197,35 @@ Outputs: Value: !Ref UseNotificationSQSQueueKMSKeyPolicy Export: Name: !Sub ${StackName}-UseNotificationSQSQueueKMSKeyPolicyArn + + # Post dated SQS outputs start here + PostDatedNotificationsSQSQueueUrl: + Description: The URL of the Post Dated Notifications SQS Queue + Value: !Ref PostDatedNotificationsSQSQueue + Export: + Name: !Sub ${StackName}-PostDatedNotificationsSQSQueueUrl + + PostDatedNotificationsSQSQueueArn: + Description: The ARN of the Post Dated Notifications SQS Queue + Value: !GetAtt PostDatedNotificationsSQSQueue.Arn + Export: + Name: !Sub ${StackName}-PostDatedNotificationsSQSQueueArn + + PostDatedNotificationsDeadLetterQueueArn: + Description: The ARN of the Post Dated Notifications Dead Letter Queue + Value: !GetAtt PostDatedNotificationsDeadLetterQueue.Arn + Export: + Name: !Sub ${StackName}-PostDatedNotificationsDeadLetterQueueArn + + ReadPostDatedNotificationsSQSQueuePolicyArn: + Description: ARN of policy granting permission to read the post dated notifications queue + Value: !Ref ReadPostDatedNotificationsSQSQueuePolicy + Export: + Name: !Sub ${StackName}-ReadPostDatedNotificationsSQSQueuePolicyArn + + WritePostDatedNotificationsSQSQueuePolicyArn: + Description: ARN of policy granting permission to write to the post dated notifications queue + Value: !Ref WritePostDatedNotificationsSQSQueuePolicy + Export: + Name: !Sub ${StackName}-WritePostDatedNotificationsSQSQueuePolicyArn + # End diff --git a/SAMtemplates/tables/main.yaml b/SAMtemplates/tables/main.yaml index a32ea6a626..75e77e2ac2 100644 --- a/SAMtemplates/tables/main.yaml +++ b/SAMtemplates/tables/main.yaml @@ -201,6 +201,25 @@ Resources: - ReadCapacityUnits: 1 WriteCapacityUnits: !Ref MinWritePrescriptionStatusUpdatesCapacity - !Ref "AWS::NoValue" + # TODO: Remove this when we deprecate post modified prescriptions. + - IndexName: PrescriptionIDPostDatedIndex + KeySchema: + - AttributeName: PrescriptionID + KeyType: HASH + Projection: + NonKeyAttributes: + - PatientNHSNumber + - LineItemID + - TerminalStatus + - LastModified + - Status + - PostDatedLastModifiedSetAt + ProjectionType: INCLUDE + ProvisionedThroughput: !If + - EnableDynamoDBAutoScalingCondition + - ReadCapacityUnits: 1 + WriteCapacityUnits: !Ref MinWritePrescriptionStatusUpdatesCapacity + - !Ref "AWS::NoValue" Tags: - Key: "EPS-Tablename" Value: "PrescriptionStatusUpdates" diff --git a/package-lock.json b/package-lock.json index 7401b81419..59292f518f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "packages/cpsuLambda", "packages/checkPrescriptionStatusUpdates", "packages/nhsNotifyLambda", + "packages/postDatedLambda", "packages/psuRestoreValidationLambda", "packages/nhsNotifyUpdateCallback", "packages/common/testing", @@ -26,6 +27,7 @@ "packages/common/utilities" ], "dependencies": { + "@aws-sdk/lib-dynamodb": "^3.975.0", "@psu-common/commonTypes": "^1.0.0", "@psu-common/middyErrorHandler": "^1.0.0", "@psu-common/utilities": "^1.0.0", @@ -225,45 +227,45 @@ } }, "node_modules/@aws-sdk/client-backup": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-backup/-/client-backup-3.987.0.tgz", - "integrity": "sha512-MQv9ul97p4YENF+ElV55RRuTabzwHDBc4Z8IRajVB/0a+U3Jvqc5aHzWQOMB3ZDQ0cgqWcKqc1ACrfuq6ORHRQ==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-backup/-/client-backup-3.988.0.tgz", + "integrity": "sha512-UZssTmSt1kQyJRr52w7GYnNcQsLOMZMDxSiTMSz5lmcUI9/PTUPWgTqBFacI04eKWlaYdSH/a8dQ2mu2fBrUHg==", "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/core": "^3.973.8", + "@aws-sdk/credential-provider-node": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.987.0", + "@aws-sdk/util-endpoints": "3.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.5", + "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.1", + "@smithy/core": "^3.23.0", "@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-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", "@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/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@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-defaults-mode-browser": "^4.3.30", + "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -275,9 +277,9 @@ } }, "node_modules/@aws-sdk/client-backup/node_modules/@aws-sdk/util-endpoints": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.987.0.tgz", - "integrity": "sha512-rZnZwDq7Pn+TnL0nyS6ryAhpqTZtLtHbJaqfxuHlDX3v/bq0M7Ch/V3qF9dZWaGgsJ2H9xn7/vFOxlnL4fBMcQ==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -291,47 +293,45 @@ } }, "node_modules/@aws-sdk/client-dynamodb": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.987.0.tgz", - "integrity": "sha512-F5Vf79D5F1VZj/m7jnTRw6y4OCuMEchtdAp0LHJxUp5I9OmOYnjfKOI/BFHduJJXcurqHLZDU41s811vRqr1yw==", + "version": "3.975.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/dynamodb-codec": "^3.972.8", - "@aws-sdk/middleware-endpoint-discovery": "^3.972.3", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.7", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.987.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.5", + "@aws-sdk/core": "^3.973.1", + "@aws-sdk/credential-provider-node": "^3.972.1", + "@aws-sdk/dynamodb-codec": "^3.972.2", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.1", + "@aws-sdk/middleware-host-header": "^3.972.1", + "@aws-sdk/middleware-logger": "^3.972.1", + "@aws-sdk/middleware-recursion-detection": "^3.972.1", + "@aws-sdk/middleware-user-agent": "^3.972.2", + "@aws-sdk/region-config-resolver": "^3.972.1", + "@aws-sdk/types": "^3.973.0", + "@aws-sdk/util-endpoints": "3.972.0", + "@aws-sdk/util-user-agent-browser": "^3.972.1", + "@aws-sdk/util-user-agent-node": "^3.972.1", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.1", + "@smithy/core": "^3.21.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.13", - "@smithy/middleware-retry": "^4.4.30", + "@smithy/middleware-endpoint": "^4.4.11", + "@smithy/middleware-retry": "^4.4.27", "@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/node-http-handler": "^4.4.8", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.10.12", "@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-defaults-mode-browser": "^4.3.26", + "@smithy/util-defaults-mode-node": "^4.2.29", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -343,62 +343,47 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/util-endpoints": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.987.0.tgz", - "integrity": "sha512-rZnZwDq7Pn+TnL0nyS6ryAhpqTZtLtHbJaqfxuHlDX3v/bq0M7Ch/V3qF9dZWaGgsJ2H9xn7/vFOxlnL4fBMcQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.987.0.tgz", - "integrity": "sha512-OV651KLxvRHNO0o2Hp1aNmhuhwq1e8WA7RMn14AjegjIE6XWnw5jtANK125BdNtpV97kMZR/WXshO9UJ/oIxWA==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.988.0.tgz", + "integrity": "sha512-uL0ofMUufIFqGO4CBkQM16EbxUda59WGv/SMRfa2tiFolZ/3zaM5c+NWfsZazGHKaF/KjcNVs3rDT8jOTV+pjQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.7", - "@aws-sdk/credential-provider-node": "^3.972.6", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/credential-provider-node": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.987.0", + "@aws-sdk/util-endpoints": "3.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.5", + "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.1", + "@smithy/core": "^3.23.0", "@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-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", "@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/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@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-defaults-mode-browser": "^4.3.30", + "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -410,9 +395,9 @@ } }, "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-endpoints": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.987.0.tgz", - "integrity": "sha512-rZnZwDq7Pn+TnL0nyS6ryAhpqTZtLtHbJaqfxuHlDX3v/bq0M7Ch/V3qF9dZWaGgsJ2H9xn7/vFOxlnL4fBMcQ==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -426,47 +411,47 @@ } }, "node_modules/@aws-sdk/client-sqs": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.987.0.tgz", - "integrity": "sha512-9X87NXIBNpeQMs8dRSWBsOxWcL0PNRkBPckQhh8Ewnch42nEUqmnTZjuLgKy0s3LDUnCCVzIdjhFsLo5vtr7zQ==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.988.0.tgz", + "integrity": "sha512-Rwh+YhdhhGPFfZ0kLDl1GkLJ0xed0rgQnlGTHiVrwFE51nZm/Is6Car7m1nE4j0Rohom06qNClP3btt0mOf/pg==", "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/core": "^3.973.8", + "@aws-sdk/credential-provider-node": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-sdk-sqs": "^3.972.6", - "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/middleware-sdk-sqs": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.987.0", + "@aws-sdk/util-endpoints": "3.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.5", + "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.1", + "@smithy/core": "^3.23.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/md5-js": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.13", - "@smithy/middleware-retry": "^4.4.30", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", "@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/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@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-defaults-mode-browser": "^4.3.30", + "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -478,9 +463,9 @@ } }, "node_modules/@aws-sdk/client-sqs/node_modules/@aws-sdk/util-endpoints": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.987.0.tgz", - "integrity": "sha512-rZnZwDq7Pn+TnL0nyS6ryAhpqTZtLtHbJaqfxuHlDX3v/bq0M7Ch/V3qF9dZWaGgsJ2H9xn7/vFOxlnL4fBMcQ==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -494,45 +479,45 @@ } }, "node_modules/@aws-sdk/client-ssm": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.987.0.tgz", - "integrity": "sha512-foDdcyE3/On2mIx2FDfF/8cVELL7Wj2JcnMwBdX29P2l3UAybuEHByEEX65lo8GwRWirifGaX4fPRFQ6KJiMDw==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.988.0.tgz", + "integrity": "sha512-Pm5ukPtUjf6R4hKgu/FPKDYR3fgEjXwMlK/q7Gtoxi8X9GETEwD8QTbo4AdMfjGFIbSbgVjHRvkOWPl+HuU8Gg==", "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/core": "^3.973.8", + "@aws-sdk/credential-provider-node": "^3.972.7", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.987.0", + "@aws-sdk/util-endpoints": "3.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.5", + "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.1", + "@smithy/core": "^3.23.0", "@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-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", "@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/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@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-defaults-mode-browser": "^4.3.30", + "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -545,9 +530,9 @@ } }, "node_modules/@aws-sdk/client-ssm/node_modules/@aws-sdk/util-endpoints": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.987.0.tgz", - "integrity": "sha512-rZnZwDq7Pn+TnL0nyS6ryAhpqTZtLtHbJaqfxuHlDX3v/bq0M7Ch/V3qF9dZWaGgsJ2H9xn7/vFOxlnL4fBMcQ==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -561,44 +546,44 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.985.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.985.0.tgz", - "integrity": "sha512-81J8iE8MuXhdbMfIz4sWFj64Pe41bFi/uqqmqOC5SlGv+kwoyLsyKS/rH2tW2t5buih4vTUxskRjxlqikTD4oQ==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.988.0.tgz", + "integrity": "sha512-ThqQ7aF1k0Zz4yJRwegHw+T1rM3a7ZPvvEUSEdvn5Z8zTeWgJAbtqW/6ejPsMLmFOlHgNcwDQN/e69OvtEOoIQ==", "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/core": "^3.973.8", "@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/middleware-user-agent": "^3.972.8", "@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.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.5", + "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.1", + "@smithy/core": "^3.23.0", "@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-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", "@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/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@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-defaults-mode-browser": "^4.3.30", + "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -609,20 +594,36 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", + "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", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.7.tgz", - "integrity": "sha512-wNZZQQNlJ+hzD49cKdo+PY6rsTDElO8yDImnrI69p2PLBa7QomeUKAJWYp9xnaR38nlHqWhMHZuYLCQ3oSX+xg==", + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.8.tgz", + "integrity": "sha512-WeYJ2sfvRLbbUIrjGMUXcEHGu5SJk53jz3K9F8vFP42zWyROzPJ2NB6lMu9vWl5hnMwzwabX7pJc9Euh3JyMGw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.4", - "@smithy/core": "^3.22.1", + "@smithy/core": "^3.23.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", @@ -634,12 +635,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.5.tgz", - "integrity": "sha512-LxJ9PEO4gKPXzkufvIESUysykPIdrV7+Ocb9yAhbhJLE4TiAYqbCVUE+VuKP1leGR1bBfjWjYgSV5MxprlX3mQ==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.6.tgz", + "integrity": "sha512-+dYEBWgTqkQQHFUllvBL8SLyXyLKWdxLMD1LmKJRvmb0NMJuaJFG/qg78C+LE67eeGbipYcE+gJ48VlLBGHlMw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.7", + "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", @@ -650,20 +651,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.7.tgz", - "integrity": "sha512-L2uOGtvp2x3bTcxFTpSM+GkwFIPd8pHfGWO1764icMbo7e5xJh0nfhx1UwkXLnwvocTNEf8A7jISZLYjUSNaTg==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.8.tgz", + "integrity": "sha512-z3QkozMV8kOFisN2pgRag/f0zPDrw96mY+ejAM0xssV/+YQ2kklbylRNI/TcTQUDnGg0yPxNjyV6F2EM2zPTwg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.7", + "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.9", + "@smithy/node-http-handler": "^4.4.10", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.11", + "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" }, "engines": { @@ -671,19 +672,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.5.tgz", - "integrity": "sha512-SdDTYE6jkARzOeL7+kudMIM4DaFnP5dZVeatzw849k4bSXDdErDS188bgeNzc/RA2WGrlEpsqHUKP6G7sVXhZg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.973.7", - "@aws-sdk/credential-provider-env": "^3.972.5", - "@aws-sdk/credential-provider-http": "^3.972.7", - "@aws-sdk/credential-provider-login": "^3.972.5", - "@aws-sdk/credential-provider-process": "^3.972.5", - "@aws-sdk/credential-provider-sso": "^3.972.5", - "@aws-sdk/credential-provider-web-identity": "^3.972.5", - "@aws-sdk/nested-clients": "3.985.0", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.6.tgz", + "integrity": "sha512-6tkIYFv3sZH1XsjQq+veOmx8XWRnyqTZ5zx/sMtdu/xFRIzrJM1Y2wAXeCJL1rhYSB7uJSZ1PgALI2WVTj78ow==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/credential-provider-env": "^3.972.6", + "@aws-sdk/credential-provider-http": "^3.972.8", + "@aws-sdk/credential-provider-login": "^3.972.6", + "@aws-sdk/credential-provider-process": "^3.972.6", + "@aws-sdk/credential-provider-sso": "^3.972.6", + "@aws-sdk/credential-provider-web-identity": "^3.972.6", + "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", @@ -696,13 +697,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.5.tgz", - "integrity": "sha512-uYq1ILyTSI6ZDCMY5+vUsRM0SOCVI7kaW4wBrehVVkhAxC6y+e9rvGtnoZqCOWL1gKjTMouvsf4Ilhc5NCg1Aw==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.6.tgz", + "integrity": "sha512-LXsoBoaTSGHdRCQXlWSA0CHHh05KWncb592h9ElklnPus++8kYn1Ic6acBR4LKFQ0RjjMVgwe5ypUpmTSUOjPA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.7", - "@aws-sdk/nested-clients": "3.985.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", @@ -715,17 +716,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.6.tgz", - "integrity": "sha512-DZ3CnAAtSVtVz+G+ogqecaErMLgzph4JH5nYbHoBMgBkwTUV+SUcjsjOJwdBJTHu3Dm6l5LBYekZoU2nDqQk2A==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.7.tgz", + "integrity": "sha512-PuJ1IkISG7ZDpBFYpGotaay6dYtmriBYuHJ/Oko4VHxh8YN5vfoWnMNYFEWuzOfyLmP7o9kDVW0BlYIpb3skvw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.5", - "@aws-sdk/credential-provider-http": "^3.972.7", - "@aws-sdk/credential-provider-ini": "^3.972.5", - "@aws-sdk/credential-provider-process": "^3.972.5", - "@aws-sdk/credential-provider-sso": "^3.972.5", - "@aws-sdk/credential-provider-web-identity": "^3.972.5", + "@aws-sdk/credential-provider-env": "^3.972.6", + "@aws-sdk/credential-provider-http": "^3.972.8", + "@aws-sdk/credential-provider-ini": "^3.972.6", + "@aws-sdk/credential-provider-process": "^3.972.6", + "@aws-sdk/credential-provider-sso": "^3.972.6", + "@aws-sdk/credential-provider-web-identity": "^3.972.6", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", @@ -738,12 +739,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.5.tgz", - "integrity": "sha512-HDKF3mVbLnuqGg6dMnzBf1VUOywE12/N286msI9YaK9mEIzdsGCtLTvrDhe3Up0R9/hGFbB+9l21/TwF5L1C6g==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.6.tgz", + "integrity": "sha512-Yf34cjIZJHVnD92jnVYy3tNjM+Q4WJtffLK2Ehn0nKpZfqd1m7SI0ra22Lym4C53ED76oZENVSS2wimoXJtChQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.7", + "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -755,14 +756,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.5.tgz", - "integrity": "sha512-8urj3AoeNeQisjMmMBhFeiY2gxt6/7wQQbEGun0YV/OaOOiXrIudTIEYF8ZfD+NQI6X1FY5AkRsx6O/CaGiybA==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.6.tgz", + "integrity": "sha512-2+5UVwUYdD4BBOkLpKJ11MQ8wQeyJGDVMDRH5eWOULAh9d6HJq07R69M/mNNMC9NTjr3mB1T0KGDn4qyQh5jzg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.985.0", - "@aws-sdk/core": "^3.973.7", - "@aws-sdk/token-providers": "3.985.0", + "@aws-sdk/client-sso": "3.988.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/token-providers": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -774,13 +775,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.5.tgz", - "integrity": "sha512-OK3cULuJl6c+RcDZfPpaK5o3deTOnKZbxm7pzhFNGA3fI2hF9yDih17fGRazJzGGWaDVlR9ejZrpDef4DJCEsw==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.6.tgz", + "integrity": "sha512-pdJzwKtlDxBnvZ04pWMqttijmkUIlwOsS0GcxCjzEVyUMpARysl0S0ks74+gs2Pdev3Ujz+BTAjOc1tQgAxGqA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.7", - "@aws-sdk/nested-clients": "3.985.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -792,14 +793,14 @@ } }, "node_modules/@aws-sdk/dynamodb-codec": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.8.tgz", - "integrity": "sha512-5ngfn6fQPSNc7G9LlingK4SXfzcJtv5pOP++erc7HmCq0LcDj//0pcpLgxpDII0sBTh0FcR/iw9i4fBZwSJ2Cg==", + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.9.tgz", + "integrity": "sha512-ITv7JRqIgSRDbwmC3rUnQ7hoyRv+oH+ww0Gz+f2z8VTlS69STSLbfNiHGJrZ3TT4QEai9QYGwhSdjRYg1btglQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.7", - "@smithy/core": "^3.22.1", - "@smithy/smithy-client": "^4.11.2", + "@aws-sdk/core": "^3.973.8", + "@smithy/core": "^3.23.0", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" @@ -810,8 +811,6 @@ }, "node_modules/@aws-sdk/endpoint-cache": { "version": "3.972.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.2.tgz", - "integrity": "sha512-3L7mwqSLJ6ouZZKtCntoNF0HTYDNs1FDQqkGjoPWXcv1p0gnLotaDmLq1rIDqfu4ucOit0Re3ioLyYDUTpSroA==", "license": "Apache-2.0", "dependencies": { "mnemonist": "0.38.3", @@ -822,15 +821,13 @@ } }, "node_modules/@aws-sdk/lib-dynamodb": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.987.0.tgz", - "integrity": "sha512-AMczivn08gcwugPB5+hHMaV1BREtHAO9Gytwx2wSZb9PYm/rCbJgeKW6T4ySMn+mocpGE3TshfIIhHTodLeYPA==", + "version": "3.975.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.7", - "@aws-sdk/util-dynamodb": "3.987.0", - "@smithy/core": "^3.22.1", - "@smithy/smithy-client": "^4.11.2", + "@aws-sdk/core": "^3.973.1", + "@aws-sdk/util-dynamodb": "3.975.0", + "@smithy/core": "^3.21.1", + "@smithy/smithy-client": "^4.10.12", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -838,13 +835,11 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@aws-sdk/client-dynamodb": "^3.987.0" + "@aws-sdk/client-dynamodb": "3.975.0" } }, "node_modules/@aws-sdk/middleware-endpoint-discovery": { "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.3.tgz", - "integrity": "sha512-xAxA8/TOygQmMrzcw9CrlpTHCGWSG/lvzrHCySfSZpDN4/yVSfXO+gUwW9WxeskBmuv9IIFATOVpzc9EzfTZ0Q==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/endpoint-cache": "^3.972.2", @@ -860,8 +855,6 @@ }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", - "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -875,8 +868,6 @@ }, "node_modules/@aws-sdk/middleware-logger": { "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", - "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -889,8 +880,6 @@ }, "node_modules/@aws-sdk/middleware-recursion-detection": { "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", - "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -904,13 +893,13 @@ } }, "node_modules/@aws-sdk/middleware-sdk-sqs": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.6.tgz", - "integrity": "sha512-6e+dZ1qPEIDO8ASIu09QNVrwXAzuLOuD5jd1M7oj41e+/wShJPn2oG8ZDYUGTnGBOrc4h1UILWYodzMzzTrkiQ==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.7.tgz", + "integrity": "sha512-DcJLYE4sRjgUyb2SupQGaRgBYc+j89N9nXeMT0PwwVvaBGmKqcxa7PFvz0kBnQrBckPWlfrPyyyMwOeT5BEp6Q==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", @@ -921,15 +910,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.7.tgz", - "integrity": "sha512-HUD+geASjXSCyL/DHPQc/Ua7JhldTcIglVAoCV8kiVm99IaFSlAbTvEnyhZwdE6bdFyTL+uIaWLaCFSRsglZBQ==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.8.tgz", + "integrity": "sha512-3PGL+Kvh1PhB0EeJeqNqOWQgipdqFheO4OUKc6aYiFwEpM5t9AyE5hjjxZ5X6iSj8JiduWFZLPwASzF6wQRgFg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.7", + "@aws-sdk/core": "^3.973.8", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.985.0", - "@smithy/core": "^3.22.1", + "@aws-sdk/util-endpoints": "3.988.0", + "@smithy/core": "^3.23.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -938,45 +927,61 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", + "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", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.985.0.tgz", - "integrity": "sha512-TsWwKzb/2WHafAY0CE7uXgLj0FmnkBTgfioG9HO+7z/zCPcl1+YU+i7dW4o0y+aFxFgxTMG+ExBQpqT/k2ao8g==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.988.0.tgz", + "integrity": "sha512-OgYV9k1oBCQ6dOM+wWAMNNehXA8L4iwr7ydFV+JDHyuuu0Ko7tDXnLEtEmeQGYRcAFU3MGasmlBkMB8vf4POrg==", "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/core": "^3.973.8", "@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/middleware-user-agent": "^3.972.8", "@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.988.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.5", + "@aws-sdk/util-user-agent-node": "^3.972.6", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.1", + "@smithy/core": "^3.23.0", "@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-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", "@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/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@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-defaults-mode-browser": "^4.3.30", + "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -987,10 +992,24 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", + "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", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", - "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -1004,13 +1023,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.985.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.985.0.tgz", - "integrity": "sha512-+hwpHZyEq8k+9JL2PkE60V93v2kNhUIv7STFt+EAez1UJsJOQDhc5LpzEX66pNjclI5OTwBROs/DhJjC/BtMjQ==", + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.988.0.tgz", + "integrity": "sha512-xvXVlRVKHnF2h6fgWBm64aPP5J+58aJyGfRrQa/uFh8a9mcK68mLfJOYq+ZSxQy/UN3McafJ2ILAy7IWzT9kRw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.7", - "@aws-sdk/nested-clients": "3.985.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/nested-clients": "3.988.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -1023,8 +1042,6 @@ }, "node_modules/@aws-sdk/types": { "version": "3.973.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", - "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -1035,9 +1052,7 @@ } }, "node_modules/@aws-sdk/util-dynamodb": { - "version": "3.987.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.987.0.tgz", - "integrity": "sha512-Th5/gzi7fPKrWlSXRlxNk15rOk9Ya07NHEubfx1pz9Em7KjF3CyLPdaXNqWR02ajBLYriS1CUnOS0dhf0e6APw==", + "version": "3.975.0", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1046,16 +1061,14 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@aws-sdk/client-dynamodb": "^3.987.0" + "@aws-sdk/client-dynamodb": "3.975.0" } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.985.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz", - "integrity": "sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA==", + "version": "3.972.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", + "@aws-sdk/types": "3.972.0", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", @@ -1065,6 +1078,17 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/util-endpoints/node_modules/@aws-sdk/types": { + "version": "3.972.0", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/util-locate-window": { "version": "3.893.0", "license": "Apache-2.0", @@ -1077,8 +1101,6 @@ }, "node_modules/@aws-sdk/util-user-agent-browser": { "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", - "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -1088,12 +1110,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.5.tgz", - "integrity": "sha512-GsUDF+rXyxDZkkJxUsDxnA67FG+kc5W1dnloCFLl6fWzceevsCYzJpASBzT+BPjwUgREE6FngfJYYYMQUY5fZQ==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.6.tgz", + "integrity": "sha512-966xH8TPqkqOXP7EwnEThcKKz0SNP9kVJBKd9M8bNXE4GSqVouMKKnFBwYnzbWVKuLXubzX5seokcX4a0JLJIA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.8", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", @@ -1127,8 +1149,6 @@ }, "node_modules/@aws/lambda-invoke-store": { "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", - "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" @@ -1159,6 +1179,7 @@ "version": "7.28.4", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1604,9 +1625,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", "dev": true, "license": "MIT", "optional": true, @@ -1616,9 +1637,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "dev": true, "license": "MIT", "optional": true, @@ -2079,8 +2100,6 @@ }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2098,8 +2117,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -2108,8 +2125,6 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2123,8 +2138,6 @@ }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2134,8 +2147,6 @@ }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", "dependencies": { @@ -2147,8 +2158,6 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2160,8 +2169,6 @@ }, "node_modules/@eslint/core": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2217,8 +2224,6 @@ }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -2241,8 +2246,6 @@ }, "node_modules/@eslint/js": { "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -2254,8 +2257,6 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2264,8 +2265,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2362,8 +2361,6 @@ }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", "dev": true, "license": "MIT", "engines": { @@ -2372,8 +2369,6 @@ }, "node_modules/@isaacs/brace-expansion": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", "dev": true, "license": "MIT", "dependencies": { @@ -2826,8 +2821,6 @@ }, "node_modules/@middy/core": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@middy/core/-/core-7.0.2.tgz", - "integrity": "sha512-C4PEJxMjWFneqfQzsbWQ9BZ7Bfds9oqw74/fTuEeunI/0PA1KtGfbHDuU1SQH6lZ3rOp+VUIYh4YDho8qmc2Rg==", "license": "MIT", "engines": { "node": ">=22" @@ -2847,8 +2840,6 @@ }, "node_modules/@middy/http-header-normalizer": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@middy/http-header-normalizer/-/http-header-normalizer-7.0.2.tgz", - "integrity": "sha512-VYxaswkxENBy3YdHkMy6dQz1rX1pTPDuF299EPnywyXNVG740EnVkBcCwpLB0Xn5OKWPCNrwISTvntTvVXmQeg==", "license": "MIT", "engines": { "node": ">=22" @@ -2860,8 +2851,6 @@ }, "node_modules/@middy/input-output-logger": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@middy/input-output-logger/-/input-output-logger-7.0.2.tgz", - "integrity": "sha512-sNOb9Ja+xbWNxpMBSl0jP6mJA66IHYBVy24TVS4PRmRDty4YCVUwkd3i+ZFTVOc91Ek6MuE03+RRIkGydBIbdQ==", "license": "MIT", "dependencies": { "@middy/util": "7.0.2" @@ -2876,8 +2865,6 @@ }, "node_modules/@middy/input-output-logger/node_modules/@middy/util": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@middy/util/-/util-7.0.2.tgz", - "integrity": "sha512-81pP1Am7QarL6e2+SHYnDu83jxqgxM3sztHJNLDZ2B+YwzT+smGjMaDX10bFH8PvTDr/nic3QaNOe2X8UoCaBw==", "license": "MIT", "engines": { "node": ">=22" @@ -2921,6 +2908,7 @@ "node_modules/@middy/validator/node_modules/ajv": { "version": "8.17.1", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3032,6 +3020,7 @@ "version": "1.9.0", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -3303,8 +3292,6 @@ }, "node_modules/@redocly/ajv": { "version": "8.17.1", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==", "dev": true, "license": "MIT", "dependencies": { @@ -3371,8 +3358,6 @@ "node_modules/@redocly/cli/node_modules/ajv": { "name": "@redocly/ajv", "version": "8.17.1", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==", "dev": true, "license": "MIT", "dependencies": { @@ -3411,8 +3396,6 @@ }, "node_modules/@redocly/cli/node_modules/glob": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3449,8 +3432,6 @@ }, "node_modules/@redocly/cli/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, @@ -3464,8 +3445,6 @@ }, "node_modules/@redocly/cli/node_modules/minimatch": { "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3495,8 +3474,6 @@ }, "node_modules/@redocly/cli/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -3827,8 +3804,6 @@ }, "node_modules/@smithy/abort-controller": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", - "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -3840,8 +3815,6 @@ }, "node_modules/@smithy/config-resolver": { "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", - "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", @@ -3856,9 +3829,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.22.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz", - "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==", + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.0.tgz", + "integrity": "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.2.9", @@ -3867,7 +3840,7 @@ "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.11", + "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -3878,8 +3851,6 @@ }, "node_modules/@smithy/credential-provider-imds": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", - "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", @@ -3894,8 +3865,6 @@ }, "node_modules/@smithy/fetch-http-handler": { "version": "5.3.9", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", - "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.8", @@ -3910,8 +3879,6 @@ }, "node_modules/@smithy/hash-node": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", - "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -3925,8 +3892,6 @@ }, "node_modules/@smithy/invalid-dependency": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", - "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -3938,8 +3903,6 @@ }, "node_modules/@smithy/is-array-buffer": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3950,8 +3913,6 @@ }, "node_modules/@smithy/md5-js": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.8.tgz", - "integrity": "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -3964,8 +3925,6 @@ }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", - "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.8", @@ -3977,12 +3936,12 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz", - "integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==", + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.14.tgz", + "integrity": "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.22.1", + "@smithy/core": "^3.23.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -3996,15 +3955,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.30", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz", - "integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==", + "version": "4.4.31", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.31.tgz", + "integrity": "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -4017,8 +3976,6 @@ }, "node_modules/@smithy/middleware-serde": { "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", - "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.3.8", @@ -4031,8 +3988,6 @@ }, "node_modules/@smithy/middleware-stack": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", - "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -4044,8 +3999,6 @@ }, "node_modules/@smithy/node-config-provider": { "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", - "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.8", @@ -4058,9 +4011,9 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz", - "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.10.tgz", + "integrity": "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.8", @@ -4075,8 +4028,6 @@ }, "node_modules/@smithy/property-provider": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", - "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -4088,8 +4039,6 @@ }, "node_modules/@smithy/protocol-http": { "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", - "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -4101,8 +4050,6 @@ }, "node_modules/@smithy/querystring-builder": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", - "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -4115,8 +4062,6 @@ }, "node_modules/@smithy/querystring-parser": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", - "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -4128,8 +4073,6 @@ }, "node_modules/@smithy/service-error-classification": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", - "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0" @@ -4140,8 +4083,6 @@ }, "node_modules/@smithy/shared-ini-file-loader": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", - "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -4153,8 +4094,6 @@ }, "node_modules/@smithy/signature-v4": { "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", - "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", @@ -4171,17 +4110,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz", - "integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==", + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.3.tgz", + "integrity": "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.22.1", - "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/core": "^3.23.0", + "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.11", + "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" }, "engines": { @@ -4190,8 +4129,6 @@ }, "node_modules/@smithy/types": { "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", - "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4202,8 +4139,6 @@ }, "node_modules/@smithy/url-parser": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", - "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", "license": "Apache-2.0", "dependencies": { "@smithy/querystring-parser": "^4.2.8", @@ -4216,8 +4151,6 @@ }, "node_modules/@smithy/util-base64": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.0", @@ -4230,8 +4163,6 @@ }, "node_modules/@smithy/util-body-length-browser": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4242,8 +4173,6 @@ }, "node_modules/@smithy/util-body-length-node": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4254,8 +4183,6 @@ }, "node_modules/@smithy/util-buffer-from": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", @@ -4267,8 +4194,6 @@ }, "node_modules/@smithy/util-config-provider": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4278,13 +4203,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.29", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz", - "integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==", + "version": "4.3.30", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.30.tgz", + "integrity": "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -4293,16 +4218,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.32", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz", - "integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==", + "version": "4.2.33", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.33.tgz", + "integrity": "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.11.2", + "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, @@ -4312,8 +4237,6 @@ }, "node_modules/@smithy/util-endpoints": { "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", - "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.8", @@ -4326,8 +4249,6 @@ }, "node_modules/@smithy/util-hex-encoding": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4338,8 +4259,6 @@ }, "node_modules/@smithy/util-middleware": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", - "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", @@ -4351,8 +4270,6 @@ }, "node_modules/@smithy/util-retry": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", - "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", "license": "Apache-2.0", "dependencies": { "@smithy/service-error-classification": "^4.2.8", @@ -4364,13 +4281,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.11", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz", - "integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==", + "version": "4.5.12", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.12.tgz", + "integrity": "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg==", "license": "Apache-2.0", "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.9", + "@smithy/node-http-handler": "^4.4.10", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", @@ -4384,8 +4301,6 @@ }, "node_modules/@smithy/util-uri-escape": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4396,8 +4311,6 @@ }, "node_modules/@smithy/util-utf8": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^4.2.0", @@ -4409,8 +4322,6 @@ }, "node_modules/@smithy/util-waiter": { "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", - "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.2.8", @@ -4423,8 +4334,6 @@ }, "node_modules/@smithy/uuid": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4466,8 +4375,6 @@ }, "node_modules/@types/aws-lambda": { "version": "8.10.160", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.160.tgz", - "integrity": "sha512-uoO4QVQNWFPJMh26pXtmtrRfGshPUSpMZGUyUQY20FhfHEElEBOPKgVmFs1z+kbpyBsRs2JnoOPT7++Z4GA9pA==", "dev": true, "license": "MIT" }, @@ -4559,6 +4466,7 @@ "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4642,6 +4550,7 @@ "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", @@ -5130,6 +5039,7 @@ "version": "8.15.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5167,6 +5077,7 @@ "node_modules/ajv": { "version": "6.12.6", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5393,6 +5304,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -5563,8 +5475,6 @@ }, "node_modules/bowser": { "version": "2.12.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", - "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -5604,6 +5514,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -6315,10 +6226,9 @@ }, "node_modules/eslint": { "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6377,6 +6287,7 @@ "version": "10.1.8", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6403,8 +6314,6 @@ }, "node_modules/eslint-plugin-prettier": { "version": "5.5.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", - "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", "dev": true, "license": "MIT", "dependencies": { @@ -6665,8 +6574,6 @@ }, "node_modules/fast-diff": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true, "license": "Apache-2.0" }, @@ -6950,8 +6857,6 @@ }, "node_modules/glob": { "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -7461,6 +7366,7 @@ "version": "30.2.0", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -8044,8 +7950,6 @@ }, "node_modules/jose": { "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -8066,8 +7970,6 @@ }, "node_modules/js-yaml": { "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -8498,8 +8400,6 @@ }, "node_modules/mnemonist": { "version": "0.38.3", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", - "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", "license": "MIT", "dependencies": { "obliterator": "^1.6.1" @@ -8509,6 +8409,7 @@ "version": "6.15.0", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -8860,8 +8761,6 @@ }, "node_modules/obliterator": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", - "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", "license": "MIT" }, "node_modules/once": { @@ -9282,6 +9181,10 @@ "dev": true, "license": "MIT" }, + "node_modules/postDatedLambda": { + "resolved": "packages/postDatedLambda", + "link": true + }, "node_modules/pratica": { "version": "2.3.0", "license": "Apache-2.0" @@ -9295,9 +9198,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "peer": true, @@ -9313,8 +9216,6 @@ }, "node_modules/prettier-linter-helpers": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", - "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { @@ -9481,20 +9382,18 @@ }, "node_modules/react": { "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", - "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", - "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9675,8 +9574,6 @@ }, "node_modules/redoc/node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -9810,8 +9707,6 @@ }, "node_modules/scheduler": { "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "dev": true, "license": "MIT" }, @@ -9824,8 +9719,6 @@ }, "node_modules/semver": { "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -10343,11 +10236,12 @@ "license": "MIT" }, "node_modules/styled-components": { - "version": "6.3.8", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.8.tgz", - "integrity": "sha512-Kq/W41AKQloOqKM39zfaMdJ4BcYDw/N5CIq4/GTI0YjU6pKcZ1KKhk6b4du0a+6RA9pIfOP/eu94Ge7cu+PDCA==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.3.9.tgz", + "integrity": "sha512-J72R4ltw0UBVUlEjTzI0gg2STOqlI9JBhQOL4Dxt7aJOnnSesy0qJDn4PYfMCafk9cWOaVg129Pesl5o+DIh0Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.4.0", "@emotion/unitless": "0.10.0", @@ -10433,8 +10327,6 @@ }, "node_modules/synckit": { "version": "0.11.12", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", - "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10501,8 +10393,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10518,8 +10408,6 @@ }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -10536,10 +10424,9 @@ }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10582,8 +10469,6 @@ }, "node_modules/ts-api-utils": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -10595,8 +10480,6 @@ }, "node_modules/ts-jest": { "version": "29.4.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", - "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", "dev": true, "license": "MIT", "dependencies": { @@ -10668,6 +10551,7 @@ "version": "10.9.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10742,10 +10626,9 @@ }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10792,8 +10675,6 @@ }, "node_modules/ulid": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/ulid/-/ulid-3.0.2.tgz", - "integrity": "sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w==", "dev": true, "license": "MIT", "bin": { @@ -10802,8 +10683,6 @@ }, "node_modules/undici": { "version": "6.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", - "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "dev": true, "license": "MIT", "engines": { @@ -10812,8 +10691,6 @@ }, "node_modules/undici-types": { "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, @@ -11343,6 +11220,111 @@ "@psu-common/testing": "^1.0.0" } }, + "packages/checkPrescriptionStatusUpdates/node_modules/@aws-sdk/client-dynamodb": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.988.0.tgz", + "integrity": "sha512-rK9seN2TS3+Iqzc958kcDIJ83n0BYGargpRyZsyh9GCiBaipelFqOIUlZ/gXZzkhFxNBLTKmdkMAioahvmQkoQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/credential-provider-node": "^3.972.7", + "@aws-sdk/dynamodb-codec": "^3.972.9", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.3", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.8", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.988.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.6", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@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.14", + "@smithy/middleware-retry": "^4.4.31", + "@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.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "packages/checkPrescriptionStatusUpdates/node_modules/@aws-sdk/lib-dynamodb": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.988.0.tgz", + "integrity": "sha512-dsd57UeAObG5dKcTRhTV1E+jXa6E3AQKaqF0QdyjF0ccTY8Ai0iWhorJ4bV3DV4J0l/k1dZJpacQblds4+GhVw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/util-dynamodb": "3.988.0", + "@smithy/core": "^3.23.0", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.988.0" + } + }, + "packages/checkPrescriptionStatusUpdates/node_modules/@aws-sdk/util-dynamodb": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.988.0.tgz", + "integrity": "sha512-4bAyzAS7x4vXDl5clGQB24CZ6H9xJP3FW+UE1iobbEc1ApZh4etQULN7u6ua0Jsw/NMdF089cknX/ksCBzbBFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.988.0" + } + }, + "packages/checkPrescriptionStatusUpdates/node_modules/@aws-sdk/util-endpoints": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", + "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" + } + }, "packages/common/commonTypes": { "name": "@psu-common/commonTypes", "version": "1.0.0", @@ -11395,6 +11377,85 @@ "aws-sdk-client-mock": "^4.1.0" } }, + "packages/cpsuLambda/node_modules/@aws-sdk/client-dynamodb": { + "version": "3.981.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/credential-provider-node": "^3.972.4", + "@aws-sdk/dynamodb-codec": "^3.972.5", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.3", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.981.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "packages/cpsuLambda/node_modules/@aws-sdk/util-dynamodb": { + "version": "3.981.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "3.981.0" + } + }, + "packages/cpsuLambda/node_modules/@aws-sdk/util-endpoints": { + "version": "3.981.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" + } + }, "packages/cpsuLambda/node_modules/uuid": { "version": "13.0.0", "funding": [ @@ -11422,6 +11483,111 @@ "json-schema-to-ts": "^3.1.1" } }, + "packages/gsul/node_modules/@aws-sdk/client-dynamodb": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.988.0.tgz", + "integrity": "sha512-rK9seN2TS3+Iqzc958kcDIJ83n0BYGargpRyZsyh9GCiBaipelFqOIUlZ/gXZzkhFxNBLTKmdkMAioahvmQkoQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/credential-provider-node": "^3.972.7", + "@aws-sdk/dynamodb-codec": "^3.972.9", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.3", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.8", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.988.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.6", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@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.14", + "@smithy/middleware-retry": "^4.4.31", + "@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.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "packages/gsul/node_modules/@aws-sdk/lib-dynamodb": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.988.0.tgz", + "integrity": "sha512-dsd57UeAObG5dKcTRhTV1E+jXa6E3AQKaqF0QdyjF0ccTY8Ai0iWhorJ4bV3DV4J0l/k1dZJpacQblds4+GhVw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/util-dynamodb": "3.988.0", + "@smithy/core": "^3.23.0", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.988.0" + } + }, + "packages/gsul/node_modules/@aws-sdk/util-dynamodb": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.988.0.tgz", + "integrity": "sha512-4bAyzAS7x4vXDl5clGQB24CZ6H9xJP3FW+UE1iobbEc1ApZh4etQULN7u6ua0Jsw/NMdF089cknX/ksCBzbBFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.988.0" + } + }, + "packages/gsul/node_modules/@aws-sdk/util-endpoints": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", + "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" + } + }, "packages/nhsd-psu-sandbox": { "version": "1.0.0", "license": "MIT", @@ -11471,6 +11637,45 @@ "axios-mock-adapter": "^2.1.0" } }, + "packages/postDatedLambda": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@aws-lambda-powertools/logger": "^2.30.0", + "@aws-sdk/client-dynamodb": "^3.817.0", + "@aws-sdk/client-sqs": "^3.817.0", + "@aws-sdk/util-dynamodb": "^3.817.0", + "@middy/core": "^6.4.5", + "@middy/input-output-logger": "^6.4.5", + "@nhs/fhir-middy-error-handler": "^2.1.65", + "@psu-common/commonTypes": "^1.0.0" + }, + "devDependencies": { + "@psu-common/testing": "^1.0.0" + } + }, + "packages/postDatedLambda/node_modules/@middy/core": { + "version": "6.4.5", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, + "packages/postDatedLambda/node_modules/@middy/input-output-logger": { + "version": "6.4.5", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, "packages/psuRestoreValidationLambda": { "version": "1.0.0", "license": "MIT", @@ -11488,8 +11693,110 @@ "@psu-common/testing": "^1.0.0" } }, - "packages/sandbox": { - "extraneous": true + "packages/psuRestoreValidationLambda/node_modules/@aws-sdk/client-dynamodb": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.988.0.tgz", + "integrity": "sha512-rK9seN2TS3+Iqzc958kcDIJ83n0BYGargpRyZsyh9GCiBaipelFqOIUlZ/gXZzkhFxNBLTKmdkMAioahvmQkoQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/credential-provider-node": "^3.972.7", + "@aws-sdk/dynamodb-codec": "^3.972.9", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.3", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.8", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.988.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.6", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@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.14", + "@smithy/middleware-retry": "^4.4.31", + "@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.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "packages/psuRestoreValidationLambda/node_modules/@aws-sdk/lib-dynamodb": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.988.0.tgz", + "integrity": "sha512-dsd57UeAObG5dKcTRhTV1E+jXa6E3AQKaqF0QdyjF0ccTY8Ai0iWhorJ4bV3DV4J0l/k1dZJpacQblds4+GhVw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.8", + "@aws-sdk/util-dynamodb": "3.988.0", + "@smithy/core": "^3.23.0", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.988.0" + } + }, + "packages/psuRestoreValidationLambda/node_modules/@aws-sdk/util-dynamodb": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.988.0.tgz", + "integrity": "sha512-4bAyzAS7x4vXDl5clGQB24CZ6H9xJP3FW+UE1iobbEc1ApZh4etQULN7u6ua0Jsw/NMdF089cknX/ksCBzbBFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.988.0" + } + }, + "packages/psuRestoreValidationLambda/node_modules/@aws-sdk/util-endpoints": { + "version": "3.988.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.988.0.tgz", + "integrity": "sha512-HuXu4boeUWU0DQiLslbgdvuQ4ZMCo4Lsk97w8BIUokql2o9MvjE5dwqI5pzGt0K7afO1FybjidUQVTMLuZNTOA==", + "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" + } }, "packages/specification": { "name": "apim-spec", @@ -11539,6 +11846,85 @@ "@types/fhir": "^0.0.41", "aws-sdk-client-mock": "^4.1.0" } + }, + "packages/updatePrescriptionStatus/node_modules/@aws-sdk/client-dynamodb": { + "version": "3.981.0", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.5", + "@aws-sdk/credential-provider-node": "^3.972.4", + "@aws-sdk/dynamodb-codec": "^3.972.5", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.3", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.5", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.981.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.3", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "packages/updatePrescriptionStatus/node_modules/@aws-sdk/util-dynamodb": { + "version": "3.981.0", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "3.981.0" + } + }, + "packages/updatePrescriptionStatus/node_modules/@aws-sdk/util-endpoints": { + "version": "3.981.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" + } } } } diff --git a/package.json b/package.json index 31ae8ece89..7f80f35152 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "packages/cpsuLambda", "packages/checkPrescriptionStatusUpdates", "packages/nhsNotifyLambda", + "packages/postDatedLambda", "packages/psuRestoreValidationLambda", "packages/nhsNotifyUpdateCallback", "packages/common/testing", @@ -50,6 +51,7 @@ "typescript-eslint": "^8.55.0" }, "dependencies": { + "@aws-sdk/lib-dynamodb": "^3.975.0", "@psu-common/commonTypes": "^1.0.0", "@psu-common/middyErrorHandler": "^1.0.0", "@psu-common/utilities": "^1.0.0", diff --git a/packages/common/commonTypes/src/index.ts b/packages/common/commonTypes/src/index.ts index 557f1ff257..808e28df60 100644 --- a/packages/common/commonTypes/src/index.ts +++ b/packages/common/commonTypes/src/index.ts @@ -22,6 +22,8 @@ export interface NotifyDataItem { RequestID: string TaskID: string Status: string + // TODO: This should be removed when we stop supporting post-dated updates + PrescriptionID: string // Needed to query NPPTS } /** diff --git a/packages/nhsNotifyLambda/src/utils/dynamo.ts b/packages/nhsNotifyLambda/src/utils/dynamo.ts index b18fe7759d..aed6a2a352 100644 --- a/packages/nhsNotifyLambda/src/utils/dynamo.ts +++ b/packages/nhsNotifyLambda/src/utils/dynamo.ts @@ -43,7 +43,7 @@ export async function addPrescriptionMessagesToNotificationStateStore( NotifyMessageReference: data.messageReference, NotifyMessageBatchReference: data.messageBatchReference, // Will be undefined when request fails LastNotificationRequestTimestamp: new Date().toISOString(), - ExpiryTime: (Math.floor(+new Date() / 1000) + TTL_DELTA) + ExpiryTime: (Math.floor(Date.now() / 1000) + TTL_DELTA) } try { diff --git a/packages/nhsNotifyLambda/tests/notify.test.ts b/packages/nhsNotifyLambda/tests/notify.test.ts index 36aa726e51..61a200a0a5 100644 --- a/packages/nhsNotifyLambda/tests/notify.test.ts +++ b/packages/nhsNotifyLambda/tests/notify.test.ts @@ -46,6 +46,7 @@ describe("logNotificationRequest", () => { const dataItem1: NotifyDataItemMessage = { messageReference: "msg-ref-1", PSUDataItem: { + PrescriptionID: "presc-1", PatientNHSNumber: "9453740578", PharmacyODSCode: "FA566", RequestID: "req-1", @@ -57,6 +58,7 @@ describe("logNotificationRequest", () => { const dataItem2: NotifyDataItemMessage = { messageReference: "msg-ref-2", PSUDataItem: { + PrescriptionID: "presc-2", PatientNHSNumber: "9912003071", PharmacyODSCode: "A83008", RequestID: "req-2", diff --git a/packages/nhsNotifyLambda/tests/testUtils.test.ts b/packages/nhsNotifyLambda/tests/testUtils.test.ts index 9029db622b..b3c2c3355d 100644 --- a/packages/nhsNotifyLambda/tests/testUtils.test.ts +++ b/packages/nhsNotifyLambda/tests/testUtils.test.ts @@ -611,6 +611,7 @@ describe("NHS notify lambda helper functions", () => { const data = [ constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "r1", PatientNHSNumber: "n1", PharmacyODSCode: "o1", @@ -620,6 +621,7 @@ describe("NHS notify lambda helper functions", () => { }), constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "r2", PatientNHSNumber: "n2", PharmacyODSCode: "o2", @@ -688,6 +690,7 @@ describe("NHS notify lambda helper functions", () => { const data = [ constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "rA", PatientNHSNumber: "nx", PharmacyODSCode: "ox", @@ -734,6 +737,7 @@ describe("NHS notify lambda helper functions", () => { const data = [ constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "rX", PatientNHSNumber: "ny", PharmacyODSCode: "oy", @@ -743,6 +747,7 @@ describe("NHS notify lambda helper functions", () => { }), constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "rY", PatientNHSNumber: "nz", PharmacyODSCode: "oz", @@ -782,6 +787,7 @@ describe("NHS notify lambda helper functions", () => { const data = Array.from({length: 7}, (_, i) => constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: `r${i}`, PatientNHSNumber: `n${i}`, PharmacyODSCode: `o${i}`, @@ -829,6 +835,7 @@ describe("NHS notify lambda helper functions", () => { const data = [ constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "r1", PatientNHSNumber: "n1", PharmacyODSCode: "o1", @@ -838,6 +845,7 @@ describe("NHS notify lambda helper functions", () => { }), constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "r2", PatientNHSNumber: "n2", PharmacyODSCode: "o2", @@ -887,6 +895,7 @@ describe("NHS notify lambda helper functions", () => { const data = [ constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "r1", PatientNHSNumber: "n1", PharmacyODSCode: "o1", @@ -896,6 +905,7 @@ describe("NHS notify lambda helper functions", () => { }), constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "r2", PatientNHSNumber: "n2", PharmacyODSCode: "o2", @@ -964,6 +974,7 @@ describe("NHS notify lambda helper functions", () => { const dataWithMixed = [ constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "r1", PatientNHSNumber: "n1", PharmacyODSCode: "o1", @@ -973,6 +984,7 @@ describe("NHS notify lambda helper functions", () => { }), constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "r2", PatientNHSNumber: "n2", PharmacyODSCode: "o2", @@ -1004,6 +1016,7 @@ describe("NHS notify lambda helper functions", () => { const data = [constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "r1", PatientNHSNumber: "n1", PharmacyODSCode: "o1", @@ -1026,6 +1039,7 @@ describe("NHS notify lambda helper functions", () => { const data = [constructPSUDataItemMessage({ PSUDataItem: { + PrescriptionID: "prescription-id", RequestID: "r1", PatientNHSNumber: "n1", PharmacyODSCode: "o1", diff --git a/packages/postDatedLambda/.jest/setEnvVars.js b/packages/postDatedLambda/.jest/setEnvVars.js new file mode 100644 index 0000000000..a82f1bb0a1 --- /dev/null +++ b/packages/postDatedLambda/.jest/setEnvVars.js @@ -0,0 +1,4 @@ +/* eslint-disable no-undef */ +process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL = "dummy_notify_sqs"; +process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = "dummy_post_dated_sqs"; +process.env.AWS_REGION = "eu-west-2"; diff --git a/packages/postDatedLambda/.vscode/launch.json b/packages/postDatedLambda/.vscode/launch.json new file mode 100644 index 0000000000..7c9b0b4b3a --- /dev/null +++ b/packages/postDatedLambda/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "name": "vscode-jest-tests.v2", + "request": "launch", + "args": [ + "--runInBand", + "--watchAll=false", + "--testNamePattern", + "${jest.testNamePattern}", + "--runTestsByPath", + "${jest.testFile}", + "--config", + "${workspaceFolder}/jest.debug.config.ts" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "program": "${workspaceFolder}/../../node_modules/.bin/jest", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + }, + "env": { + "POWERTOOLS_DEV": true, + "NODE_OPTIONS": "--experimental-vm-modules" + } + } + ] +} diff --git a/packages/postDatedLambda/.vscode/settings.json b/packages/postDatedLambda/.vscode/settings.json new file mode 100644 index 0000000000..3501264944 --- /dev/null +++ b/packages/postDatedLambda/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "jest.jestCommandLine": "/workspaces/eps-prescription-status-update-api/node_modules/.bin/jest --no-cache", + "jest.nodeEnv": { + "POWERTOOLS_DEV": true, + "NODE_OPTIONS": "--experimental-vm-modules" + } +} diff --git a/packages/postDatedLambda/docs/mature_prescription_check.md b/packages/postDatedLambda/docs/mature_prescription_check.md new file mode 100644 index 0000000000..3d572a60de --- /dev/null +++ b/packages/postDatedLambda/docs/mature_prescription_check.md @@ -0,0 +1,35 @@ +This is the business logic used for checking if a prescription ID has matured or not. It needs to tolerate a mixture of post-dated, and contemporary prescription updates; e.g. "ready to collect", post-dated, followed by "with pharmacy" non-post-dated. + +The lambda drains the post-dated SQS queue in batches and handles each message based on the result of `determineAction()`. Immature prescriptions will be re-processed after some delay by changing their visibility timeout. Some prescriptions may no longer need to be considered (e.g. if there has been a subsequent PSU request marking it as "with pharmacy" again), so are deleted from the post-dated SQS without being forwarded to the notifications SQS. Mature prescriptions are forwarded to the notifications SQS. +```mermaid +flowchart TB + START["Scheduled EventBridge trigger"] --> REPORT["Report post-dated queue status"] + REPORT --> LOOP{"Within max runtime?
Still messages to process?"} + LOOP -- No --> EXIT["Exit and report queue status"] + LOOP -- Yes --> RECV["Receive up to 10 SQS messages"] + RECV --> ENRICH["Enrich messages with most recent NPPTS record"] + ENRICH --> DETERMINE["Run `determineAction()` per message"] + DETERMINE -- FORWARD_TO_NOTIFICATIONS --> FORWARD["Forward to Notifications queue"] + DETERMINE -- REPROCESS --> REPROCESS["Change visibility timeout (reprocess later)"] + DETERMINE -- REMOVE_FROM_PD_QUEUE --> REMOVE["Remove from post-dated queue"] + FORWARD --> REMOVE + REPROCESS --> LOOP + REMOVE --> LOOP +``` + +The `determineAction()` function accepts the SQS message for this prescription ID, enriched with the most recent NPPTS record for this prescription ID. It checks whether that record is post-dated and still in a notifiable status, then compares `LastModified` (i.e. the time that a post-dated update will transition) to the current time to determine whether the update is still immature or has matured. The decision of if a notification needs to be sent to this patient is handled by the notifications lambda, still. + +```mermaid +flowchart TD + A["Start `determineAction()`"] --> E{"mostRecentRecord present?"} + E -- No --> F["Log error + return REMOVE_FROM_PD_QUEUE"] + E -- Yes --> H{"PostDatedLastModifiedSetAt present?"} + H -- No --> I["Log non post-dated + return REMOVE_FROM_PD_QUEUE"] + H -- Yes --> K{"Status in [ready to collect, ready to collect - partial]?"} + K -- No --> L["Log non-notifiable + return REMOVE_FROM_PD_QUEUE"] + K -- Yes --> M["mostRecentLastModified = Date(LastModified)"] + M --> N["currentTime = new Date()"] + N --> O{"mostRecentLastModified AFTER currentTime?"} + O -- Yes --> P["return REPROCESS"] + O -- No --> Q["return FORWARD_TO_NOTIFICATIONS"] +``` diff --git a/packages/postDatedLambda/jest.config.ts b/packages/postDatedLambda/jest.config.ts new file mode 100644 index 0000000000..8212011ed9 --- /dev/null +++ b/packages/postDatedLambda/jest.config.ts @@ -0,0 +1,10 @@ +import type {JestConfigWithTsJest} from "ts-jest" +import defaultConfig from "../../jest.default.config.ts" + +const jestConfig: JestConfigWithTsJest = { + ...defaultConfig, + rootDir: "./", + setupFiles: ["/.jest/setEnvVars.js"] +} + +export default jestConfig diff --git a/packages/postDatedLambda/jest.debug.config.ts b/packages/postDatedLambda/jest.debug.config.ts new file mode 100644 index 0000000000..a306273831 --- /dev/null +++ b/packages/postDatedLambda/jest.debug.config.ts @@ -0,0 +1,9 @@ +import config from "./jest.config" +import type {JestConfigWithTsJest} from "ts-jest" + +const debugConfig: JestConfigWithTsJest = { + ...config, + "preset": "ts-jest" +} + +export default debugConfig diff --git a/packages/postDatedLambda/package.json b/packages/postDatedLambda/package.json new file mode 100644 index 0000000000..2ee890cca0 --- /dev/null +++ b/packages/postDatedLambda/package.json @@ -0,0 +1,30 @@ +{ + "name": "postDatedLambda", + "version": "1.0.0", + "description": "A lambda that handles the post-dated prescription logic for the notifications service", + "main": "main.js", + "author": "NHS Digital", + "license": "MIT", + "type": "module", + "scripts": { + "unit": "POWERTOOLS_DEV=true NODE_OPTIONS=--experimental-vm-modules jest --no-cache --coverage", + "lint": "eslint --max-warnings 0 --fix --config ../../eslint.config.mjs .", + "compile": "tsc", + "test": "npm run compile && npm run unit", + "check-licenses": "license-checker --failOn GPL --failOn LGPL --start ../..", + "cli:test-token": "npm run compile && node lib/cli/test-token-exchange.js" + }, + "dependencies": { + "@aws-lambda-powertools/logger": "^2.30.0", + "@aws-sdk/client-dynamodb": "^3.817.0", + "@aws-sdk/client-sqs": "^3.817.0", + "@aws-sdk/util-dynamodb": "^3.817.0", + "@middy/core": "^6.4.5", + "@middy/input-output-logger": "^6.4.5", + "@nhs/fhir-middy-error-handler": "^2.1.65", + "@psu-common/commonTypes": "^1.0.0" + }, + "devDependencies": { + "@psu-common/testing": "^1.0.0" + } +} diff --git a/packages/postDatedLambda/src/businessLogic.ts b/packages/postDatedLambda/src/businessLogic.ts new file mode 100644 index 0000000000..2367d87e69 --- /dev/null +++ b/packages/postDatedLambda/src/businessLogic.ts @@ -0,0 +1,118 @@ +import {Logger} from "@aws-lambda-powertools/logger" + +import {PSUDataItem} from "@psu-common/commonTypes" + +import {PostDatedSQSMessageWithRecentDataItem, PostDatedProcessingResult} from "./types" + +// This is only used in the dynamo handler, but since it's part of the core logic of determining maturity, +// it felt wrong to put it in the database client file +export function getMostRecentRecord( + existingRecords: Array +): PSUDataItem { + return existingRecords.reduce((latest, record) => { + const latestTimestamp = latest.PostDatedLastModifiedSetAt + ? new Date(latest.PostDatedLastModifiedSetAt) + : new Date(latest.LastModified) + const recordTimestamp = record.PostDatedLastModifiedSetAt + ? new Date(record.PostDatedLastModifiedSetAt) + : new Date(record.LastModified) + return recordTimestamp > latestTimestamp ? record : latest + }, existingRecords[0]) +} + +/** + * Process a single post-dated prescription message. + * A flow diagram of this logic is available at ../docs/mature_prescription_check.md + * + * @param logger - The AWS Lambda Powertools logger instance + * @param message - The SQS message containing post-dated prescription data and existing records + * @returns Promise - true if the post-dated prescription has matured, and false otherwise + */ +export function determineAction( + logger: Logger, + message: PostDatedSQSMessageWithRecentDataItem +): PostDatedProcessingResult { + logger.info("Processing post-dated prescription message", { + messageId: message.MessageId, + prescriptionData: message.prescriptionData, + mostRecentRecord: message.mostRecentRecord + }) + + // The existingRecords array contains all records from the DynamoDB table + // that match this prescription's PrescriptionID + + // NOTE: It is technically possible for this to be undefined if no existing records are found + // This SHOULD never happen in practice, but catch it anyway + if (!message.mostRecentRecord) { + logger.error("No existing records found for post-dated prescription, cannot process. Ignoring this message", { + badMessage: message + }) + + // throw new Error("No existing records found for post-dated prescription") // maybe? + return PostDatedProcessingResult.REMOVE_FROM_PD_QUEUE + } + + // Is it post-dated? + if (!message.mostRecentRecord.PostDatedLastModifiedSetAt) { + logger.info( + "Most recent record is not marked as post-dated, and will have been processed " + + "by the standard logic already. Marking as to be ignored by the post-dated notifications lambda." + ) + return PostDatedProcessingResult.REMOVE_FROM_PD_QUEUE + } + + // Is it still RTC? + const mostRecentStatus = message.mostRecentRecord.Status.toLowerCase() + const notifiableStatuses: Array = ["ready to collect", "ready to collect - partial"] + if (!notifiableStatuses.includes(mostRecentStatus)) { + logger.info("Most recent status in the NPPTS data store is not a notifiable status, so will be ignored", { + mostRecentStatus: mostRecentStatus + }) + return PostDatedProcessingResult.REMOVE_FROM_PD_QUEUE + } + + // We know that we have a recent, post-dated prescription status update. + // Check if its LastModified time is in the future. + + // Stored as YYYY-MM-DDTHH:mm:ss.sssZ + const mostRecentLastModified = new Date(message.mostRecentRecord.LastModified) + const currentTime = new Date() + logger.info("Most recent NPPTS record is Post-dated. Checking if the post-dated prescription has matured", { + LastModified: mostRecentLastModified.toISOString(), + currentTime: currentTime.toISOString() + }) + + if (mostRecentLastModified > currentTime) { + logger.info("Post-dated prescription is still immature (LastModified is in the future)", { + lastModified: mostRecentLastModified.toISOString(), + currentTime: currentTime.toISOString() + }) + return PostDatedProcessingResult.REPROCESS + } + + logger.info("Post-dated prescription has matured (LastModified is in the past)", { + lastModified: mostRecentLastModified.toISOString(), + currentTime: currentTime.toISOString() + }) + + return PostDatedProcessingResult.FORWARD_TO_NOTIFICATIONS +} + +/** + * returns time in seconds until maturity, or undefined if cannot be determined + */ +export function computeTimeUntilMaturity( + data: PostDatedSQSMessageWithRecentDataItem +): number | undefined { + const prescriptionRecord = data.mostRecentRecord + + // catches both no existing record, and one that's not post-dated. + if (!prescriptionRecord?.PostDatedLastModifiedSetAt) { + return undefined + } + + const lastModified = new Date(prescriptionRecord.LastModified) + const currentTime = Date.now() + + return (lastModified.getTime() - currentTime) / 1000 +} diff --git a/packages/postDatedLambda/src/databaseClient.ts b/packages/postDatedLambda/src/databaseClient.ts new file mode 100644 index 0000000000..030976ffe9 --- /dev/null +++ b/packages/postDatedLambda/src/databaseClient.ts @@ -0,0 +1,165 @@ +import {Logger} from "@aws-lambda-powertools/logger" +import {DynamoDBClient, QueryCommand, QueryCommandInput} from "@aws-sdk/client-dynamodb" +import {unmarshall} from "@aws-sdk/util-dynamodb" + +import {PSUDataItem, NotifyDataItem} from "@psu-common/commonTypes" +import {PostDatedSQSMessage, PostDatedSQSMessageWithRecentDataItem} from "./types" +import {getMostRecentRecord} from "./businessLogic" + +const client = new DynamoDBClient() +const tableName = process.env.TABLE_NAME ?? "PrescriptionStatusUpdates" +const pharmacyPrescriptionIndexName = "PrescriptionIDPostDatedIndex" + +type PrescriptionLookupRequest = { + lookupKey: string + prescriptionID: string +} + +/** + * Query the PrescriptionStatusUpdates table for all records matching a given prescription ID. + * There should always be at least one result, but there may be multiple if the prescription has been + * updated multiple times. + * + * @param prescriptionID - The prescription ID to query for + * @param logger - The AWS Lambda Powertools logger instance + * @returns Array of PSUDataItem records matching the prescription ID. Sorted by LastModified descending. + */ +export async function getRecentDataItemByPrescriptionID( + prescriptionID: string, + logger: Logger +): Promise> { + const normalizedPrescriptionID = prescriptionID.toUpperCase() + + // Use the GSI to query by PrescriptionID + const query: QueryCommandInput = { + TableName: tableName, + IndexName: pharmacyPrescriptionIndexName, + KeyConditionExpression: "PrescriptionID = :pid", + ExpressionAttributeValues: { + ":pid": {S: normalizedPrescriptionID} + } + } + + let lastEvaluatedKey + let items: Array = [] + + logger.info("Querying DynamoDB for existing prescription records", { + prescriptionID: normalizedPrescriptionID, + tableName, + indexName: pharmacyPrescriptionIndexName + }) + + try { + while (true) { + if (lastEvaluatedKey) { + query.ExclusiveStartKey = lastEvaluatedKey + } + + const result = await client.send(new QueryCommand(query)) + + if (result.Items) { + const parsedItems: Array = result.Items.map((item) => unmarshall(item) as PSUDataItem) + items.push(...parsedItems) + } + + lastEvaluatedKey = result.LastEvaluatedKey + if (!lastEvaluatedKey) { + break + } + } + + logger.info("Retrieved existing prescription records from DynamoDB", { + prescriptionID: normalizedPrescriptionID, + recordCount: items.length + }) + + return items + } catch (err) { + logger.error("Error querying DynamoDB for existing prescription records", { + prescriptionID: normalizedPrescriptionID, + error: err + }) + throw err + } +} + +function buildLookupRequests(postDatedItems: Array): Array { + // Run though a map to deduplicate lookups + const lookups = new Map() + + for (const item of postDatedItems) { + const lookupKey = item.PrescriptionID.toUpperCase() // Case insensitive + + // dont worry about overwriting entries, since they'll be identical + lookups.set(lookupKey, { + lookupKey, + prescriptionID: item.PrescriptionID + }) + } + + return Array.from(lookups.values()) +} + +async function buildRecentDataItemMap( + lookupRequests: Array, + logger: Logger +): Promise>> { + const RecentDataItemMap = new Map>() + + // await all lookups in parallel + await Promise.all( + lookupRequests.map(async ({lookupKey, prescriptionID}) => { + try { + const records = await getRecentDataItemByPrescriptionID(prescriptionID, logger) + RecentDataItemMap.set(lookupKey, records) + } catch (error) { + logger.error("Failed to fetch existing records for prescription", { + prescriptionID, + error + }) + RecentDataItemMap.set(lookupKey, []) // Continue processing other prescriptions even when one fails + } + }) + ) + + return RecentDataItemMap +} + +/** + * Enrich SQS messages with existing records from DynamoDB. + * For each prescription ID in the messages, fetches any matching records from the table. + * + * @param messages - Array of SQS messages to enrich + * @param logger - Logger instance + * @returns Array of messages enriched with a mostRecentRecord field containing the most + * recent matching record from DynamoDB, if any. If no matching records are found, + * mostRecentRecord will be undefined. + */ +export async function enrichMessagesWithMostRecentDataItem( + messages: Array, + logger: Logger +): Promise> { + if (messages.length === 0) { + return [] + } + + const postDatedItems = messages.map((message) => message.prescriptionData) + + // There may be repeated prescription IDs in the messages. + // Use a map to dedupe the lookups to dynamo, submit them all in async, then map the results back to the messages. + const lookupRequests = buildLookupRequests(postDatedItems) + const recentDataItemMap = await buildRecentDataItemMap(lookupRequests, logger) + + const enrichedMessages: Array = messages.map((message) => { + const lookupKey = message.prescriptionData.PrescriptionID.toUpperCase() + const existingRecords = recentDataItemMap.get(lookupKey) ?? [] + const mostRecentRecord = existingRecords.length > 0 ? getMostRecentRecord(existingRecords) : undefined + + return { + ...message, + mostRecentRecord + } + }) + + return enrichedMessages +} diff --git a/packages/postDatedLambda/src/main.ts b/packages/postDatedLambda/src/main.ts new file mode 100644 index 0000000000..b2f97fd3e2 --- /dev/null +++ b/packages/postDatedLambda/src/main.ts @@ -0,0 +1,34 @@ +import {EventBridgeEvent} from "aws-lambda" +import {Logger} from "@aws-lambda-powertools/logger" +import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" + +import middy from "@middy/core" +import inputOutputLogger from "@middy/input-output-logger" +import errorHandler from "@nhs/fhir-middy-error-handler" + +import {reportQueueStatus} from "./sqs" +import {processPostDatedQueue} from "./orchestration" + +const logger = new Logger({serviceName: "postDatedLambda"}) + +/** + * Handler for the scheduled EventBridge trigger. + */ +export const lambdaHandler = async ( + event: EventBridgeEvent +): Promise => { + logger.info("Post-dated handling lambda triggered by scheduler", {event}) + + // Report queue status *before* processing + await reportQueueStatus(logger) + + // work through the queue + await processPostDatedQueue(logger) +} + +export const handler = middy(lambdaHandler) + .use(injectLambdaContext(logger, {clearState: true})) + .use( + inputOutputLogger({logger: (req) => logger.info(req)}) + ) + .use(errorHandler({logger})) diff --git a/packages/postDatedLambda/src/orchestration.ts b/packages/postDatedLambda/src/orchestration.ts new file mode 100644 index 0000000000..d7c205f099 --- /dev/null +++ b/packages/postDatedLambda/src/orchestration.ts @@ -0,0 +1,110 @@ +import {Logger} from "@aws-lambda-powertools/logger" + +import {computeTimeUntilMaturity, determineAction} from "./businessLogic" +import {enrichMessagesWithMostRecentDataItem} from "./databaseClient" +import { + receivePostDatedSQSMessages, + reportQueueStatus, + forwardSQSMessageToNotificationQueue, + removeSQSMessage, + returnMessageToQueue +} from "./sqs" +import {PostDatedProcessingResult, PostDatedSQSMessage, PostDatedSQSMessageWithRecentDataItem} from "./types" + +export const MAX_QUEUE_RUNTIME = 14 * 60 * 1000 // 14 minutes, to avoid Lambda timeout issues (timeout is 15 minutes) +const MIN_RECEIVED_THRESHOLD = 3 // If fewer than this number of messages are received, consider the queue empty + +async function handleMaturedPrescription( + logger: Logger, + message: PostDatedSQSMessageWithRecentDataItem +): Promise { + await forwardSQSMessageToNotificationQueue(logger, message) + await removeSQSMessage(logger, message) +} + +async function handleImmaturePrescription( + logger: Logger, + message: PostDatedSQSMessageWithRecentDataItem +): Promise { + // Set visibility timeout to time until maturity, or default if calculation fails + message.visibilityTimeoutSeconds = computeTimeUntilMaturity(message) + await returnMessageToQueue(logger, message) +} + +/** + * Process a batch of SQS messages. Returns arrays of matured and immature prescription updates. + * Messages are enriched with existing records from DynamoDB and processed individually. + * Results are tracked for success/failure handling. + * + * @param messages - Array of messages to process + * @param logger - Logger instance + * @returns Object containing arrays of successful and failed messages + */ +export async function processMessages( + messages: Array, + logger: Logger +): Promise { + const enrichedMessages = await enrichMessagesWithMostRecentDataItem(messages, logger) + + // Build an array of promises to await in parallel + const promises = [] + for (const message of enrichedMessages) { + const action = determineAction(logger, message) + + switch (action) { + case PostDatedProcessingResult.REPROCESS: + promises.push(handleImmaturePrescription(logger, message)) + break + case PostDatedProcessingResult.FORWARD_TO_NOTIFICATIONS: + promises.push(handleMaturedPrescription(logger, message)) + break + case PostDatedProcessingResult.REMOVE_FROM_PD_QUEUE: + promises.push(removeSQSMessage(logger, message)) + break + default: + logger.error("Unexpected processing result", { + messageId: message.MessageId, + action + }) + throw new Error(`Unexpected processing result: ${action}`) + } + } + + await Promise.all(promises) +} + +/** + * Drain the queue until empty or the MAX_QUEUE_RUNTIME has passed. + * Messages are processed in batches of 10. + * + * @param logger - Logger instance + */ +export async function processPostDatedQueue(logger: Logger): Promise { + const start = Date.now() + let empty = false + + while (!empty) { + // Lambdas can only run for so long, so guard against that + if (Date.now() - start >= MAX_QUEUE_RUNTIME) { + logger.warn("processPostDatedQueue timed out; exiting before queue is empty. Will report queue status", { + maxRuntimeMilliseconds: MAX_QUEUE_RUNTIME + }) + await reportQueueStatus(logger) + break + } + + const messages = await receivePostDatedSQSMessages(logger) + + // break condition + if (messages.length < MIN_RECEIVED_THRESHOLD) { + empty = true + logger.info("Received fewer messages than minimum threshold; considering queue drained", { + receivedMessageCount: messages.length, + minimumThreshold: MIN_RECEIVED_THRESHOLD + }) + } + + // Process messages for this batch + await processMessages(messages, logger) + } +} diff --git a/packages/postDatedLambda/src/sqs.ts b/packages/postDatedLambda/src/sqs.ts new file mode 100644 index 0000000000..92c03631e2 --- /dev/null +++ b/packages/postDatedLambda/src/sqs.ts @@ -0,0 +1,318 @@ +import { + SQSClient, + ReceiveMessageCommand, + DeleteMessageBatchCommand, + ChangeMessageVisibilityBatchCommand, + GetQueueAttributesCommand, + SendMessageBatchCommand, + SendMessageBatchRequestEntry, + Message +} from "@aws-sdk/client-sqs" +import {Logger} from "@aws-lambda-powertools/logger" + +import {NotifyDataItem} from "@psu-common/commonTypes" + +import {PostDatedSQSMessage} from "./types" + +const sqs = new SQSClient({region: process.env.AWS_REGION}) + +const DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300 // 5 minutes +const MAXIMUM_VISIBILITY_TIMEOUT_SECONDS = 10 * 60 * 60 // 10 hours + +// Note that a lot of the code to send an SQS message is copied from the updatePrescriptionStatus lambda, +// and I've NOT moved the code into a shared location for the two. +// This is because I don't want to alter the updatePrescriptionStatus lambda in that way +// for the sake of temporarily supporting post-dated messages. +// - Jim Wild, Jan. 2026 + +function buildNotificationBatchEntry( + message: PostDatedSQSMessage, + logger: Logger +): SendMessageBatchRequestEntry { + const {prescriptionData} = message + + // If we get something with no deduplication ID, then something upstream is wrong and we should fail out + if (!message.Attributes?.MessageDeduplicationId) { + logger.error("Post-dated SQS message is missing MessageDeduplicationId attribute", { + messageId: message.MessageId, messageContents: message + }) + throw new Error("Missing MessageDeduplicationId in SQS message attributes") + } + // Same for group ID + if (!message.Attributes?.MessageGroupId) { + logger.error("Post-dated SQS message is missing MessageGroupId attribute", { + messageId: message.MessageId, messageContents: message + }) + throw new Error("Missing MessageGroupId in SQS message attributes") + } + + return { + Id: message.MessageId!, + MessageBody: JSON.stringify(prescriptionData), + MessageDeduplicationId: message.Attributes?.MessageDeduplicationId, + MessageGroupId: message.Attributes?.MessageGroupId, + MessageAttributes: { + RequestId: { + DataType: "String", + StringValue: message.Attributes?.MessageGroupId + } + } + } +} + +/** Send an entry to an SQS queue. Returns the message ID if successful, throw otherwise */ +async function sendEntryToQueue( + entry: SendMessageBatchRequestEntry, + queueUrl: string, + logger: Logger +): Promise { + + logger.info( + "Pushing a notification request to SQS", + { + deduplicationId: entry.MessageDeduplicationId, + queueUrl + } + ) + + const command = new SendMessageBatchCommand({ + QueueUrl: queueUrl, + Entries: [entry] + }) + const result = await sqs.send(command) + + if (result.Failed && result.Failed.length > 0) { + logger.error("Failed to send message to notification queue", {failed: result.Failed}) + throw new Error(`Failed to send message to notification queue: ${JSON.stringify(result.Failed)}`) + } + + // It may be that the send was successful but we didn't get a message ID back, + // which shouldn't happen but if it does we should catch it and log an error rather than returning undefined + const sentMessageId = result.Successful?.[0].MessageId + if (!sentMessageId) { + logger.error("No message ID returned from SQS for successful send", {result}) + throw new Error("No message ID returned from SQS for successful send") + } + + logger.info("Successfully sent message to notification queue", {sentMessageId}) + return sentMessageId +} + +/** + * Get the SQS queue URL from environment variables. + * Throws an error if not configured. + */ +export function getPostDatedQueueUrl(logger: Logger): string { + const sqsUrl = process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL + if (!sqsUrl) { + logger.error("Post-dated prescriptions SQS URL not configured") + throw new Error("POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL not set") + } + return sqsUrl +} + +export function getNotificationQueueUrl(logger: Logger): string { + const sqsUrl = process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL + if (!sqsUrl) { + logger.error("NHS Notify prescriptions SQS URL not configured") + throw new Error("NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL not set") + } + return sqsUrl +} + +/** + * Report the current status of the post-dated prescriptions SQS queue. + * + * @param logger - The AWS logging object + */ +export async function reportQueueStatus(logger: Logger): Promise { + const sqsUrl = getPostDatedQueueUrl(logger) + + const attrsCmd = new GetQueueAttributesCommand({ + QueueUrl: sqsUrl, + AttributeNames: [ + "ApproximateNumberOfMessages", + "ApproximateNumberOfMessagesNotVisible", + "ApproximateNumberOfMessagesDelayed" + ] + }) + const {Attributes} = await sqs.send(attrsCmd) + + // Fall back to a negative value so missing data can be identified + const ApproximateNumberOfMessages = Number.parseInt(Attributes?.ApproximateNumberOfMessages ?? "-1") + const ApproximateNumberOfMessagesNotVisible = Number.parseInt( + Attributes?.ApproximateNumberOfMessagesNotVisible ?? "-1" + ) + const ApproximateNumberOfMessagesDelayed = Number.parseInt(Attributes?.ApproximateNumberOfMessagesDelayed ?? "-1") + + logger.info( + "Current post-dated queue attributes (if a value failed to fetch, it will be reported as -1):", + { + ApproximateNumberOfMessages, + ApproximateNumberOfMessagesNotVisible, + ApproximateNumberOfMessagesDelayed + } + ) +} + +/** + * Pulls up to 10 messages from SQS. + * + * @param logger - The AWS logging object + * @returns - The array of parsed PostDatedSQSMessage + */ +export async function receivePostDatedSQSMessages(logger: Logger): Promise> { + const sqsUrl = getPostDatedQueueUrl(logger) + const toFetch = 10 + + const receiveCmd = new ReceiveMessageCommand({ + QueueUrl: sqsUrl, + MaxNumberOfMessages: toFetch, + // Use long polling to avoid getting empty responses when the queue is small + WaitTimeSeconds: 20, + MessageAttributeNames: ["All"], + MessageSystemAttributeNames: ["MessageDeduplicationId", "MessageGroupId"] + }) + + const {Messages} = await sqs.send(receiveCmd) + + if (!Messages || Messages.length === 0) { + logger.info("No messages received; marking queue as empty") + return [] + } + + logger.info("Received some messages from the post-dated queue. Parsing them...", { + MessageIDs: Messages.map((m) => m.MessageId) + }) + + const parsedMessages: Array = Messages.flatMap((m) => { + if (!m.Body) { + logger.error( + "Received an invalid SQS message (missing Body) - omitting from processing.", + {offendingMessage: m} + ) + return [] + } + try { + const parsedBody: NotifyDataItem = JSON.parse(m.Body) + return [ + { + ...m, + prescriptionData: parsedBody + } + ] + } catch (error) { + logger.error( + "Failed to parse SQS message body as JSON - omitting from processing.", + {offendingMessage: m, parseError: error} + ) + return [] + } + }) + + logger.info(`In sum, retrieved ${parsedMessages.length} messages from post-dated SQS`, { + MessageIDs: parsedMessages.map((el) => el.MessageId) + }) + + return parsedMessages +} + +/** + * Forward matured post-dated messages to the Notify queue using the same payload as the update lambda. + */ +export async function forwardSQSMessageToNotificationQueue( + logger: Logger, + message: PostDatedSQSMessage +): Promise { + const queueUrl = getNotificationQueueUrl(logger) + const entry = buildNotificationBatchEntry(message, logger) + + return sendEntryToQueue(entry, queueUrl, logger) +} + +/** + * Delete successfully processed message from the SQS queue. + * + * @param logger - The logging object + * @param message - The message that should be deleted + */ +export async function removeSQSMessage( + logger: Logger, + message: Message +): Promise { + const sqsUrl = getPostDatedQueueUrl(logger) + + const entries = [{ + Id: message.MessageId!, + ReceiptHandle: message.ReceiptHandle! + }] + + const deleteCmd = new DeleteMessageBatchCommand({ + QueueUrl: sqsUrl, + Entries: entries + }) + const delResult = await sqs.send(deleteCmd) + + if (delResult.Failed && delResult.Failed.length > 0) { + logger.error("Some messages failed to delete", {failed: delResult.Failed}) + } else { + logger.info("Successfully deleted SQS messages", { + result: delResult, + messageIds: entries.map((e) => e.Id) + }) + } + + logger.info(`Successfully removed ${delResult.Successful?.length ?? 0} messages from SQS`) +} + +/** + * Edit failed that are on the queue to update their visibility timeout. + * This makes the messages invisible for the specified duration before they can be processed again. + * This does not delete the messages, or post new ones; it only alters their visibility. + * + * @param logger - The logging object + * @param messages - The messages that failed processing and should be returned to the queue + */ +export async function returnMessageToQueue( + logger: Logger, + message: PostDatedSQSMessage +): Promise { + const sqsUrl = getPostDatedQueueUrl(logger) + + const timeout = Math.max(0, Math.min( // greater than 0 + message.visibilityTimeoutSeconds || DEFAULT_VISIBILITY_TIMEOUT_SECONDS, // fallback + MAXIMUM_VISIBILITY_TIMEOUT_SECONDS // limit + )) + + const entries = [{ + Id: message.MessageId!, + ReceiptHandle: message.ReceiptHandle!, + VisibilityTimeout: timeout + }] + + logger.info( + `Returning message to queue with timeouts`, + {sqsMessage: message, visibilityTimeout: timeout} + ) + + const changeVisibilityCmd = new ChangeMessageVisibilityBatchCommand({ + QueueUrl: sqsUrl, + Entries: entries + }) + + try { + const result = await sqs.send(changeVisibilityCmd) + + if (result.Failed && result.Failed.length > 0) { + logger.error("Some messages failed to have visibility changed in this batch", {failed: result.Failed}) + } else { + logger.info("Successfully returned SQS messages to queue", { + result: result, + messageIds: entries.map((e) => e.Id) + }) + } + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to change SQS message visibility" + logger.error(message, {error}) + } +} diff --git a/packages/postDatedLambda/src/types.ts b/packages/postDatedLambda/src/types.ts new file mode 100644 index 0000000000..46d5889bad --- /dev/null +++ b/packages/postDatedLambda/src/types.ts @@ -0,0 +1,37 @@ +import {Message} from "@aws-sdk/client-sqs" +import {NotifyDataItem, PSUDataItem} from "@psu-common/commonTypes" + +/** + * Extended SQS message interface for post-dated prescription messages. + * Contains the parsed prescription data from the message body. + */ +export interface PostDatedSQSMessage extends Message { + prescriptionData: NotifyDataItem + visibilityTimeoutSeconds?: number +} + +/** + * Combines post-dated prescription data from SQS with any existing + * records from the PrescriptionStatusUpdates DynamoDB table. + */ +export interface PostDatedPrescriptionWithRecentDataItem { + /** The post-dated prescription data from the SQS message */ + postDatedData: NotifyDataItem + /** The most recently submitted PSU data item for this prescription ID */ + mostRecentRecord?: PSUDataItem +} + +/** + * Extended SQS message interface that includes existing records from DynamoDB. + * Used during processing to have access to both the SQS message and related database records. + */ +export interface PostDatedSQSMessageWithRecentDataItem extends PostDatedSQSMessage { + /** The most recently submitted PSU data item for this prescription ID */ + mostRecentRecord?: PSUDataItem +} + +export enum PostDatedProcessingResult { + FORWARD_TO_NOTIFICATIONS = "forward_to_notifications", + REPROCESS = "reprocess", + REMOVE_FROM_PD_QUEUE = "remove_from_pd_queue" +} diff --git a/packages/postDatedLambda/tests/testBusinessLogic.test.ts b/packages/postDatedLambda/tests/testBusinessLogic.test.ts new file mode 100644 index 0000000000..095a1593ef --- /dev/null +++ b/packages/postDatedLambda/tests/testBusinessLogic.test.ts @@ -0,0 +1,242 @@ +import { + expect, + describe, + it, + afterEach, + jest +} from "@jest/globals" + +import {Logger} from "@aws-lambda-powertools/logger" + +import {PSUDataItem} from "@psu-common/commonTypes" + +import {PostDatedProcessingResult, PostDatedSQSMessageWithRecentDataItem} from "../src/types" +import {createMockPostModifiedDataItem} from "./testUtils" + +type BusinessLogicModule = typeof import("../src/businessLogic") + +const ORIGINAL_ENV = {...process.env} + +async function loadBusinessLogic( + envOverrides = {} +): Promise { + // Makes sure that the environment is set before import each time + jest.resetModules() + process.env = {...ORIGINAL_ENV, ...envOverrides} + return import("../src/businessLogic") +} + +function createPSURecord(overrides: Partial = {}): PSUDataItem { + const baseRecord: PSUDataItem = { + LastModified: new Date("2026-01-01T00:00:00.000Z").toISOString(), + LineItemID: "line-item", + PatientNHSNumber: "0123456789", + PharmacyODSCode: "ABC123", + PrescriptionID: "RX123", + RepeatNo: 0, + RequestID: "req-123", + Status: "ready to collect", + TaskID: "task-123", + TerminalStatus: "terminal", + ApplicationName: "post-dated-tests", + ExpiryTime: 0, + PostDatedLastModifiedSetAt: "2026-01-01T00:00:00.000Z" + } + + return { + ...baseRecord, + ...overrides + } +} + +function createMessage( + overrides: Partial = {} +): PostDatedSQSMessageWithRecentDataItem { + const prescData = createMockPostModifiedDataItem({}) + const baseMessage: PostDatedSQSMessageWithRecentDataItem = { + MessageId: "msg-123", + ReceiptHandle: "receipt-123", + Body: JSON.stringify(prescData), + Attributes: { + + }, + prescriptionData: prescData, + // In theory, this should contain the record corresponding to prescData, but for testing purposes it's fine + mostRecentRecord: createPSURecord() + } + + return { + ...baseMessage, + ...overrides, + prescriptionData: overrides.prescriptionData ?? baseMessage.prescriptionData, + mostRecentRecord: Object.hasOwn(overrides, "mostRecentRecord") + ? overrides.mostRecentRecord + : baseMessage.mostRecentRecord + } +} + +afterEach(() => { + process.env = {...ORIGINAL_ENV} + jest.useRealTimers() +}) + +describe("businessLogic", () => { + describe("getMostRecentRecord", () => { + it("should return the record with the latest timestamp, when all records are post-dated", async () => { + const {getMostRecentRecord} = await loadBusinessLogic() + const records = [ + createPSURecord({ + LineItemID: "line-old", + PostDatedLastModifiedSetAt: "2026-01-01T00:00:00.000Z", + LastModified: "2026-01-02T00:00:00.000Z" // The time it's scheduled to mature + }), + createPSURecord({ + LineItemID: "line-new", + PostDatedLastModifiedSetAt: "2026-01-01T12:00:00.000Z", // submitted 12 hours later + LastModified: "2026-01-01T18:00:00.000Z" // but last modified is earlier + }) + ] + + const result = getMostRecentRecord(records) + + expect(result.LineItemID).toBe("line-new") + }) + + it("Should return the latest record when only one record is post-dated", async () => { + const {getMostRecentRecord} = await loadBusinessLogic() + const records = [ + createPSURecord({ // post-dated record submitted first + LineItemID: "line-old", + PostDatedLastModifiedSetAt: "2026-01-01T00:00:00.000Z", + LastModified: "2026-01-15T00:00:00.000Z" // The time it's scheduled to mature + }), + createPSURecord({ // contemporary record submitted second + LineItemID: "line-new", + PostDatedLastModifiedSetAt: undefined, + LastModified: "2026-01-02T00:00:00.000Z" // The time the prescription was actually ready to collect + }) + ] + + const result = getMostRecentRecord(records) + + expect(result.LineItemID).toBe("line-new") + }) + + it("should return the latest record when no records are post-dated", async () => { + const {getMostRecentRecord} = await loadBusinessLogic() + const records = [ + createPSURecord({ + LineItemID: "line-old", + PostDatedLastModifiedSetAt: undefined, + LastModified: "2026-01-01T00:00:00.000Z" + }), + createPSURecord({ + LineItemID: "line-new", + PostDatedLastModifiedSetAt: undefined, + LastModified: "2026-02-01T00:00:00.000Z" + }) + ] + + const result = getMostRecentRecord(records) + + expect(result.LineItemID).toBe("line-new") + }) + }) + + describe("determineAction", () => { + it("should remove messages that have no existing records", async () => { + const {determineAction} = await loadBusinessLogic() + const logger = new Logger({serviceName: "post-dated-tests"}) + const message = createMessage({mostRecentRecord: undefined}) + + const result = determineAction(logger, message) + + expect(result).toBe(PostDatedProcessingResult.REMOVE_FROM_PD_QUEUE) + }) + + it("should remove messages when the most recent record is not post-dated", async () => { + const {determineAction} = await loadBusinessLogic() + const logger = new Logger({serviceName: "post-dated-tests"}) + const message = createMessage({ + mostRecentRecord: createPSURecord({ + LineItemID: "line-no-post-date", + PostDatedLastModifiedSetAt: undefined + }) + }) + + const result = determineAction(logger, message) + + expect(result).toBe(PostDatedProcessingResult.REMOVE_FROM_PD_QUEUE) + }) + + it("should remove messages when the status is not notifiable", async () => { + const {determineAction} = await loadBusinessLogic() + const logger = new Logger({serviceName: "post-dated-tests"}) + const message = createMessage({ + mostRecentRecord: createPSURecord({ + Status: "dispensed", + LineItemID: "line-not-notifiable" + }) + }) + + const result = determineAction(logger, message) + + expect(result).toBe(PostDatedProcessingResult.REMOVE_FROM_PD_QUEUE) + }) + + it("should classify a message as immature when LastModified is in the future", async () => { + jest.useFakeTimers() + jest.setSystemTime(new Date("2026-01-01T12:00:00.000Z")) + const {determineAction} = await loadBusinessLogic() + const logger = new Logger({serviceName: "post-dated-tests"}) + const message = createMessage({ + mostRecentRecord: createPSURecord({ + LastModified: "2026-01-02T12:00:00.000Z", + PostDatedLastModifiedSetAt: "2026-01-02T12:00:00.000Z", + LineItemID: "line-future" + }) + }) + + const result = determineAction(logger, message) + + expect(result).toBe(PostDatedProcessingResult.REPROCESS) + }) + + it("should classify a message as matured when LastModified is in the past", async () => { + jest.useFakeTimers() + jest.setSystemTime(new Date("2026-01-03T12:00:00.000Z")) + const {determineAction} = await loadBusinessLogic() + const logger = new Logger({serviceName: "post-dated-tests"}) + const message = createMessage({ + mostRecentRecord: createPSURecord({ + LastModified: "2026-01-02T12:00:00.000Z", + PostDatedLastModifiedSetAt: "2026-01-02T12:00:00.000Z", + LineItemID: "line-past" + }) + }) + + const result = determineAction(logger, message) + + expect(result).toBe(PostDatedProcessingResult.FORWARD_TO_NOTIFICATIONS) + }) + + it("should use the provided most recent record when determining maturity", async () => { + jest.useFakeTimers() + jest.setSystemTime(new Date("2026-01-05T12:00:00.000Z")) + const {determineAction} = await loadBusinessLogic() + const logger = new Logger({serviceName: "post-dated-tests"}) + const message = createMessage({ + mostRecentRecord: createPSURecord({ + LineItemID: "line-new", + LastModified: "2026-01-06T12:00:00.000Z", + PostDatedLastModifiedSetAt: "2026-01-06T12:00:00.000Z", + Status: "ready to collect" + }) + }) + + const result = determineAction(logger, message) + + expect(result).toBe(PostDatedProcessingResult.REPROCESS) + }) + }) +}) diff --git a/packages/postDatedLambda/tests/testDatabaseClient.test.ts b/packages/postDatedLambda/tests/testDatabaseClient.test.ts new file mode 100644 index 0000000000..5d2e938ea7 --- /dev/null +++ b/packages/postDatedLambda/tests/testDatabaseClient.test.ts @@ -0,0 +1,264 @@ +import { + expect, + describe, + it, + jest +} from "@jest/globals" + +import * as dynamo from "@aws-sdk/client-dynamodb" + +import {Logger} from "@aws-lambda-powertools/logger" + +import {createMockPostModifiedDataItem} from "./testUtils" + +// Uses unstable jest method to enable mocking while using ESM. To be replaced in future. +export function mockDynamoDBClient() { + const mockSend = jest.fn() + jest.unstable_mockModule("@aws-sdk/client-dynamodb", () => { + return { + ...dynamo, + DynamoDBClient: jest.fn().mockImplementation(() => ({ + send: mockSend + })) + } + }) + return {mockSend} +} + +const {mockSend} = mockDynamoDBClient() +const { + getRecentDataItemByPrescriptionID, + enrichMessagesWithMostRecentDataItem +} = await import("../src/databaseClient") + +const logger = new Logger({serviceName: "postDatedLambdaTEST"}) + +describe("databaseClient", () => { + beforeEach(() => { + mockSend.mockReset() + }) + + describe("getRecentDataItemByPrescriptionID", () => { + it("should return existing records from DynamoDB", async () => { + const prescriptionID = "testPrescID" + + // Mock DynamoDB response + const mockItems = [ + { + PrescriptionID: {S: prescriptionID}, + Status: {S: "With pharmacy"}, + LastModified: {S: "2024-01-01T12:00:00Z"} + }, + { + PrescriptionID: {S: prescriptionID}, + Status: {S: "Ready to collect"}, + LastModified: {S: "2023-12-31T12:00:00Z"} + } + ] + + mockSend.mockReturnValueOnce({ + Items: mockItems, + LastEvaluatedKey: undefined + }) + + const records = await getRecentDataItemByPrescriptionID( + prescriptionID, + logger + ) + + expect(records).toHaveLength(2) + expect(records[0].Status).toBe("With pharmacy") + expect(records[1].Status).toBe("Ready to collect") + }) + + it("should return an empty array when DynamoDB returns no items", async () => { + const prescriptionID = "noRecordsPrescID" + + mockSend.mockReturnValueOnce({ + Items: [], + LastEvaluatedKey: undefined + }) + + const records = await getRecentDataItemByPrescriptionID(prescriptionID, logger) + + expect(records).toEqual([]) + }) + + it("Should log and throw an error if the DynamoDB query fails", async () => { + const prescriptionID = "errorPrescID" + + // Mock DynamoDB to throw an error + const mockError = new Error("DynamoDB query failed") + mockSend.mockReturnValueOnce(Promise.reject(mockError)) + + await expect( + getRecentDataItemByPrescriptionID( + prescriptionID, + logger + ) + ).rejects.toThrow("DynamoDB query failed") + }) + + it("should paginate through multiple DynamoDB result pages", async () => { + const prescriptionID = "pagedPrescID" + + const firstPageItems = [ + { + PrescriptionID: {S: prescriptionID}, + Status: {S: "Ready to collect"}, + LastModified: {S: "2024-01-01T12:00:00Z"} + } + ] + + const secondPageItems = [ + { + PrescriptionID: {S: prescriptionID}, + Status: {S: "With pharmacy"}, + LastModified: {S: "2024-01-02T12:00:00Z"} + } + ] + + mockSend + .mockReturnValueOnce({ + Items: firstPageItems, + LastEvaluatedKey: { + PrescriptionID: {S: prescriptionID} + } + }) + .mockReturnValueOnce({ + Items: secondPageItems, + LastEvaluatedKey: undefined + }) + + const records = await getRecentDataItemByPrescriptionID(prescriptionID, logger) + + expect(mockSend).toHaveBeenCalledTimes(2) + expect(records).toHaveLength(2) + expect(records[0].Status).toBe("Ready to collect") + expect(records[1].Status).toBe("With pharmacy") + }) + }) + + describe("enrichMessagesWithMostRecentDataItem", () => { + it("should enrich messages with the most recent record", async () => { + const prescriptions = [ + createMockPostModifiedDataItem({PrescriptionID: "presc1", PharmacyODSCode: "pharmA"}), + createMockPostModifiedDataItem({PrescriptionID: "presc2", PharmacyODSCode: "pharmB"}) + ] + + // Mock DynamoDB responses + const mockItemsPresc1 = [ + { + PrescriptionID: {S: "presc1"}, + PharmacyODSCode: {S: "pharmA"}, + Status: {S: "With pharmacy"}, + LastModified: {S: "2024-01-01T12:00:00Z"} + }, + { + PrescriptionID: {S: "presc1"}, + PharmacyODSCode: {S: "pharmA"}, + Status: {S: "Ready to collect"}, + LastModified: {S: "2024-01-03T12:00:00Z"} + } + ] + + const mockItemsPresc2 = [ + { + PrescriptionID: {S: "presc2"}, + PharmacyODSCode: {S: "pharmB"}, + Status: {S: "Ready to collect"}, + LastModified: {S: "2024-01-02T12:00:00Z"} + } + ] + + mockSend + .mockReturnValueOnce({ + Items: mockItemsPresc1, + LastEvaluatedKey: undefined + }) + .mockReturnValueOnce({ + Items: mockItemsPresc2, + LastEvaluatedKey: undefined + }) + + const messages = prescriptions.map((presc) => ({ + prescriptionData: presc + })) + + const enrichedMessages = await enrichMessagesWithMostRecentDataItem( + messages, + logger + ) + + expect(enrichedMessages.length).toBe(2) + expect(enrichedMessages[0].mostRecentRecord?.Status).toBe("Ready to collect") + expect(enrichedMessages[1].mostRecentRecord?.Status).toBe("Ready to collect") + }) + + it("should return an empty array when no messages are provided", async () => { + const enrichedMessages = await enrichMessagesWithMostRecentDataItem([], logger) + + expect(enrichedMessages).toEqual([]) + }) + + it("should set mostRecentRecord to undefined when DynamoDB has no matches", async () => { + const prescriptions = [ + createMockPostModifiedDataItem({PrescriptionID: "noPresc1", PharmacyODSCode: "pharmA"}), + createMockPostModifiedDataItem({PrescriptionID: "noPresc2", PharmacyODSCode: "pharmB"}) + ] + + mockSend + .mockReturnValueOnce({ + Items: [], + LastEvaluatedKey: undefined + }) + .mockReturnValueOnce({ + Items: [], + LastEvaluatedKey: undefined + }) + + const messages = prescriptions.map((presc) => ({ + prescriptionData: presc + })) + + const enrichedMessages = await enrichMessagesWithMostRecentDataItem(messages, logger) + + expect(enrichedMessages).toHaveLength(2) + expect(enrichedMessages[0].mostRecentRecord).toBeUndefined() + expect(enrichedMessages[1].mostRecentRecord).toBeUndefined() + }) + + it("should keep processing when one prescription lookup fails", async () => { + const prescriptions = [ + createMockPostModifiedDataItem({PrescriptionID: "presc1", PharmacyODSCode: "pharmA"}), + createMockPostModifiedDataItem({PrescriptionID: "errorPresc", PharmacyODSCode: "errorPharm"}) + ] + + const mockItemsPresc1 = [ + { + PrescriptionID: {S: "presc1"}, + PharmacyODSCode: {S: "pharmA"}, + Status: {S: "With pharmacy"}, + LastModified: {S: "2024-01-01T12:00:00Z"} + } + ] + + mockSend + .mockReturnValueOnce({ + Items: mockItemsPresc1, + LastEvaluatedKey: undefined + }) + .mockReturnValueOnce(Promise.reject(new Error("DynamoDB query failed"))) + + const messages = prescriptions.map((presc) => ({ + prescriptionData: presc + })) + + const enrichedMessages = await enrichMessagesWithMostRecentDataItem(messages, logger) + + expect(enrichedMessages).toHaveLength(2) + expect(enrichedMessages[0].mostRecentRecord?.Status).toBe("With pharmacy") + expect(enrichedMessages[1].mostRecentRecord).toBeUndefined() + }) + }) +}) diff --git a/packages/postDatedLambda/tests/testMain.test.ts b/packages/postDatedLambda/tests/testMain.test.ts new file mode 100644 index 0000000000..6df8c8d468 --- /dev/null +++ b/packages/postDatedLambda/tests/testMain.test.ts @@ -0,0 +1,78 @@ +import { + jest, + describe, + it, + beforeAll, + afterEach +} from "@jest/globals" + +const mockReportQueueStatus = jest.fn() +jest.unstable_mockModule( + "../src/sqs", + async () => ({ + __esModule: true, + reportQueueStatus: mockReportQueueStatus + }) +) + +const mockProcessPostDatedQueue = jest.fn() +jest.unstable_mockModule( + "../src/orchestration", + async () => ({ + __esModule: true, + processPostDatedQueue: mockProcessPostDatedQueue + }) +) + +let lambdaHandler: typeof import("../src/main").lambdaHandler +beforeAll(async () => { + ({lambdaHandler} = await import("../src/main")) +}) + +import {mockEventBridgeEvent} from "@psu-common/testing" + +const ORIGINAL_ENV = {...process.env} + +describe("Unit test for post-dated lambda handler", () => { + afterEach(() => { + process.env = {...ORIGINAL_ENV} + + jest.clearAllMocks() + jest.restoreAllMocks() + }) + + it("should run the lambda handler successfully", async () => { + mockReportQueueStatus.mockImplementation(() => Promise.resolve()) + mockProcessPostDatedQueue.mockImplementation(() => Promise.resolve()) + + await expect(lambdaHandler(mockEventBridgeEvent)).resolves.toBeUndefined() + + expect(mockReportQueueStatus).toHaveBeenCalledTimes(1) + expect(mockProcessPostDatedQueue).toHaveBeenCalledTimes(1) + }) + + it("Should handle errors from reportQueueStatus", async () => { + mockReportQueueStatus.mockImplementation(() => { + throw new Error("Dynamo error") + }) + mockProcessPostDatedQueue.mockImplementation(() => Promise.resolve()) + + await expect(lambdaHandler(mockEventBridgeEvent)).rejects.toThrow("Dynamo error") + + expect(mockReportQueueStatus).toHaveBeenCalledTimes(1) + expect(mockProcessPostDatedQueue).not.toHaveBeenCalled() + }) + + it("Should handle errors from processPostDatedQueue", async () => { + mockReportQueueStatus.mockImplementation(() => Promise.resolve()) + mockProcessPostDatedQueue.mockImplementation(() => { + throw new Error("Processing error") + }) + + await expect(lambdaHandler(mockEventBridgeEvent)).rejects.toThrow("Processing error") + + expect(mockReportQueueStatus).toHaveBeenCalledTimes(1) + expect(mockProcessPostDatedQueue).toHaveBeenCalledTimes(1) + }) + +}) diff --git a/packages/postDatedLambda/tests/testOrchestration.test.ts b/packages/postDatedLambda/tests/testOrchestration.test.ts new file mode 100644 index 0000000000..0dbed78a38 --- /dev/null +++ b/packages/postDatedLambda/tests/testOrchestration.test.ts @@ -0,0 +1,221 @@ +import { + expect, + describe, + it, + jest +} from "@jest/globals" + +// Mock the imports from local modules +const mockDetermineAction = jest.fn() +const mockComputeTimeUntilMaturity = jest.fn().mockReturnValue(300) +jest.unstable_mockModule("../src/businessLogic", () => { + return { + determineAction: mockDetermineAction, + computeTimeUntilMaturity: mockComputeTimeUntilMaturity + } +}) + +const mockEnrichMessagesWithMostRecentDataItem = jest.fn() +jest.unstable_mockModule("../src/databaseClient", () => { + return { + enrichMessagesWithMostRecentDataItem: mockEnrichMessagesWithMostRecentDataItem + } +}) + +const mockReceivePostDatedSQSMessages = jest.fn() +const mockReportQueueStatus = jest.fn() +const mockForwardSQSMessageToNotificationQueue = jest.fn() +const mockRemoveSQSMessage = jest.fn() +const mockReturnMessageToQueue = jest.fn() +jest.unstable_mockModule("../src/sqs", () => { + return { + receivePostDatedSQSMessages: mockReceivePostDatedSQSMessages, + reportQueueStatus: mockReportQueueStatus, + forwardSQSMessageToNotificationQueue: mockForwardSQSMessageToNotificationQueue, + removeSQSMessage: mockRemoveSQSMessage, + returnMessageToQueue: mockReturnMessageToQueue + } +}) + +import {Logger} from "@aws-lambda-powertools/logger" + +import {createMockPostModifiedDataItem} from "./testUtils" +import {PostDatedProcessingResult, PostDatedSQSMessage, PostDatedSQSMessageWithRecentDataItem} from "../src/types" + +// Import the orchestration module after mocking dependencies +const {processMessages, processPostDatedQueue} = await import("../src/orchestration") + +const logger = new Logger({serviceName: "postDatedLambdaTEST"}) + +// I needed to move these functions out of the describe block since it was too deeply nested. +function createBatch(ids: Array): Array { + return ids.map((id) => ({ + MessageId: id, + Body: `Message ${id}`, + prescriptionData: createMockPostModifiedDataItem({}) + })) +} + +function enrich(messages: Array): Array { + return messages.map((message) => ({ + ...message, + mostRecentRecord: undefined + })) +} + +describe("orchestration", () => { + describe("processMessages", () => { + beforeEach(() => { + jest.clearAllMocks() + mockForwardSQSMessageToNotificationQueue.mockReturnValue(Promise.resolve("forwarded-id")) + mockRemoveSQSMessage.mockReturnValue(Promise.resolve()) + mockReturnMessageToQueue.mockReturnValue(Promise.resolve()) + }) + + it("should process messages and categorize them correctly", async () => { + const mockMessages: Array = [ + {MessageId: "1", Body: "Message 1", prescriptionData: createMockPostModifiedDataItem({})}, + {MessageId: "2", Body: "Message 2", prescriptionData: createMockPostModifiedDataItem({})}, + {MessageId: "3", Body: "Message 3", prescriptionData: createMockPostModifiedDataItem({})} + ] + + // Mock the enrichment function to return the same messages + mockEnrichMessagesWithMostRecentDataItem.mockReturnValueOnce(enrich(mockMessages)) + + // Mock determineAction to return action for each message + mockDetermineAction + .mockReturnValueOnce(PostDatedProcessingResult.FORWARD_TO_NOTIFICATIONS) + .mockReturnValueOnce(PostDatedProcessingResult.REPROCESS) + .mockReturnValueOnce(PostDatedProcessingResult.REMOVE_FROM_PD_QUEUE) + + await processMessages(mockMessages, logger) + expect(mockForwardSQSMessageToNotificationQueue).toHaveBeenCalledTimes(1) + expect(mockReturnMessageToQueue).toHaveBeenCalledTimes(1) + expect(mockRemoveSQSMessage).toHaveBeenCalledTimes(2) + }) + + it("should handle empty message array", async () => { + mockEnrichMessagesWithMostRecentDataItem.mockReturnValueOnce([]) + + await processMessages([], logger) + + expect(mockForwardSQSMessageToNotificationQueue).not.toHaveBeenCalled() + expect(mockReturnMessageToQueue).not.toHaveBeenCalled() + expect(mockRemoveSQSMessage).not.toHaveBeenCalled() + }) + + it("should pass enriched records into processMessage", async () => { + const mockMessages: Array = [ + {MessageId: "1", Body: "Message 1", prescriptionData: createMockPostModifiedDataItem({})} + ] + + const enrichedMessage: PostDatedSQSMessageWithRecentDataItem = { + ...mockMessages[0], + mostRecentRecord: undefined + } + + mockEnrichMessagesWithMostRecentDataItem.mockReturnValueOnce([enrichedMessage]) + mockDetermineAction.mockReturnValue(PostDatedProcessingResult.FORWARD_TO_NOTIFICATIONS) + + await processMessages(mockMessages, logger) + + expect(mockDetermineAction).toHaveBeenCalledWith(logger, enrichedMessage) + }) + }) + + describe("processPostDatedQueue", () => { + beforeEach(() => { + jest.clearAllMocks() + mockForwardSQSMessageToNotificationQueue.mockReturnValue(Promise.resolve("forwarded-id")) + mockRemoveSQSMessage.mockReturnValue(Promise.resolve()) + mockReturnMessageToQueue.mockReturnValue(Promise.resolve()) + }) + + it("should process the SQS queue correctly", async () => { + const mockMessages: Array = [ + {MessageId: "1", Body: "Message 1", prescriptionData: createMockPostModifiedDataItem({})}, + {MessageId: "2", Body: "Message 2", prescriptionData: createMockPostModifiedDataItem({})} + ] + + const mockEnrichedMessages = mockMessages.map((message) => ({ + ...message, + mostRecentRecord: undefined + })) + + mockReceivePostDatedSQSMessages.mockReturnValueOnce(mockMessages) + mockEnrichMessagesWithMostRecentDataItem.mockReturnValueOnce(mockEnrichedMessages) + mockDetermineAction.mockReturnValue(PostDatedProcessingResult.FORWARD_TO_NOTIFICATIONS) + + await processPostDatedQueue(logger) + + expect(mockReceivePostDatedSQSMessages).toHaveBeenCalledWith(logger) + expect(mockReportQueueStatus).not.toHaveBeenCalled() + expect(mockForwardSQSMessageToNotificationQueue).toHaveBeenCalledTimes(mockMessages.length) + expect(mockRemoveSQSMessage).toHaveBeenCalledTimes(mockMessages.length) + expect(mockReturnMessageToQueue).not.toHaveBeenCalled() + expect(mockDetermineAction).toHaveBeenCalledTimes(mockMessages.length) + }) + + it("Should stop processing if the max runtime is exceeded", async () => { + jest.useFakeTimers() + const mockMessages: Array = [ + {MessageId: "1", Body: "Message 1", prescriptionData: createMockPostModifiedDataItem({})}, + {MessageId: "2", Body: "Message 2", prescriptionData: createMockPostModifiedDataItem({})}, + {MessageId: "3", Body: "Message 3", prescriptionData: createMockPostModifiedDataItem({})}, + {MessageId: "4", Body: "Message 4", prescriptionData: createMockPostModifiedDataItem({})}, + {MessageId: "5", Body: "Message 5", prescriptionData: createMockPostModifiedDataItem({})}, + {MessageId: "6", Body: "Message 6", prescriptionData: createMockPostModifiedDataItem({})} + ] + + mockReceivePostDatedSQSMessages.mockReturnValue(mockMessages) + mockEnrichMessagesWithMostRecentDataItem.mockReturnValue(enrich(mockMessages)) + const {MAX_QUEUE_RUNTIME} = await import("../src/orchestration") + mockDetermineAction.mockReturnValue(PostDatedProcessingResult.FORWARD_TO_NOTIFICATIONS) + + const promise = processPostDatedQueue(logger) + // Overrun by a second + jest.advanceTimersByTime(MAX_QUEUE_RUNTIME + 1000) + await promise + + expect(mockReportQueueStatus).toHaveBeenCalled() + jest.useRealTimers() + }) + + it("should continue processing batches until message count drops below threshold", async () => { + const batch1 = createBatch(["1", "2", "3"]) + const batch2 = createBatch(["4", "5", "6"]) + const batch3 = createBatch(["7"]) + + mockReceivePostDatedSQSMessages + .mockReturnValueOnce(batch1) + .mockReturnValueOnce(batch2) + .mockReturnValueOnce(batch3) + mockEnrichMessagesWithMostRecentDataItem + .mockReturnValueOnce(enrich(batch1)) + .mockReturnValueOnce(enrich(batch2)) + .mockReturnValueOnce(enrich(batch3)) + mockDetermineAction.mockReturnValue(PostDatedProcessingResult.FORWARD_TO_NOTIFICATIONS) + + await processPostDatedQueue(logger) + + expect(mockReceivePostDatedSQSMessages).toHaveBeenCalledTimes(3) + expect(mockForwardSQSMessageToNotificationQueue) + .toHaveBeenCalledTimes(batch1.length + batch2.length + batch3.length) + expect(mockRemoveSQSMessage).toHaveBeenCalledTimes(batch1.length + batch2.length + batch3.length) + expect(mockReportQueueStatus).not.toHaveBeenCalled() + const totalMessages = batch1.length + batch2.length + batch3.length + expect(mockDetermineAction).toHaveBeenCalledTimes(totalMessages) + }) + + it("should treat empty receives as drained batches", async () => { + mockReceivePostDatedSQSMessages.mockReturnValueOnce([]) + mockEnrichMessagesWithMostRecentDataItem.mockReturnValueOnce([]) + + await processPostDatedQueue(logger) + + expect(mockForwardSQSMessageToNotificationQueue).not.toHaveBeenCalled() + expect(mockRemoveSQSMessage).not.toHaveBeenCalled() + expect(mockReturnMessageToQueue).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/postDatedLambda/tests/testSqs.test.ts b/packages/postDatedLambda/tests/testSqs.test.ts new file mode 100644 index 0000000000..94cdd74a6d --- /dev/null +++ b/packages/postDatedLambda/tests/testSqs.test.ts @@ -0,0 +1,315 @@ +import { + describe, + it, + expect, + jest +} from "@jest/globals" +import {SpiedFunction} from "jest-mock" + +import {Logger} from "@aws-lambda-powertools/logger" +import {LogItemMessage, LogItemExtraInput} from "@aws-lambda-powertools/logger/lib/cjs/types/Logger" +import * as sqs from "@aws-sdk/client-sqs" +import {PostDatedSQSMessage} from "../src/types" +import {createMockPostModifiedDataItem} from "./testUtils" + +export function mockSQSClient() { + const mockSend = jest.fn() + jest.unstable_mockModule("@aws-sdk/client-sqs", () => { + return { + ...sqs, + SQSClient: jest.fn().mockImplementation(() => ({ + send: mockSend + })) + } + }) + return {mockSend} +} + +const {mockSend} = mockSQSClient() + +const { + getPostDatedQueueUrl, + reportQueueStatus, + receivePostDatedSQSMessages, + removeSQSMessage, + returnMessageToQueue, + forwardSQSMessageToNotificationQueue +} = await import("../src/sqs") + +const ORIGINAL_ENV = {...process.env} + +describe("sqs", () => { + let logger: Logger + let infoSpy: SpiedFunction<(input: LogItemMessage, ...extraInput: LogItemExtraInput) => void> + let errorSpy: SpiedFunction<(input: LogItemMessage, ...extraInput: LogItemExtraInput) => void> + // let warnSpy: SpiedFunction<(input: LogItemMessage, ...extraInput: LogItemExtraInput) => void> + + beforeEach(() => { + jest.resetModules() + jest.clearAllMocks() + + // Reset environment + process.env = {...ORIGINAL_ENV} + delete process.env.SQS_SALT + + // Fresh logger and spies + logger = new Logger({serviceName: "test-service"}) + infoSpy = jest.spyOn(logger, "info") + errorSpy = jest.spyOn(logger, "error") + // warnSpy = jest.spyOn(logger, "warn") + }) + + describe("getPostDatedQueueUrl", () => { + it("Should return the SQS queue URL from environment variables", () => { + const testUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue" + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = testUrl + + const result = getPostDatedQueueUrl(logger) + expect(result).toBe(testUrl) + }) + + it("Should throw an error if the SQS queue URL is not configured", () => { + delete process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL + + expect(() => getPostDatedQueueUrl(logger)).toThrow("POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL not set") + expect(errorSpy).toHaveBeenCalledWith("Post-dated prescriptions SQS URL not configured") + }) + }) + + describe("reportQueueStatus", () => { + it("Should report the current status of the SQS queue", async () => { + const testUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue" + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = testUrl + + // Mock SQS response + mockSend.mockReturnValueOnce({ + Attributes: { + ApproximateNumberOfMessages: "5", + ApproximateNumberOfMessagesNotVisible: "2", + ApproximateNumberOfMessagesDelayed: "1" + } + }) + + await reportQueueStatus(logger) + + expect(mockSend).toHaveBeenCalledTimes(1) + expect(infoSpy).toHaveBeenCalledWith( + "Current post-dated queue attributes (if a value failed to fetch, it will be reported as -1):", + { + ApproximateNumberOfMessages: 5, + ApproximateNumberOfMessagesNotVisible: 2, + ApproximateNumberOfMessagesDelayed: 1 + } + ) + }) + + it("Should handle missing attributes gracefully", async () => { + const testUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue" + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = testUrl + + // Mock SQS response with missing attributes + mockSend.mockReturnValueOnce({ + Attributes: {} + }) + + await reportQueueStatus(logger) + + expect(mockSend).toHaveBeenCalledTimes(1) + expect(infoSpy).toHaveBeenCalledWith( + "Current post-dated queue attributes (if a value failed to fetch, it will be reported as -1):", + { + ApproximateNumberOfMessages: -1, + ApproximateNumberOfMessagesNotVisible: -1, + ApproximateNumberOfMessagesDelayed: -1 + } + ) + }) + }) + + describe("receivePostDatedSQSMessages", () => { + it("Should receive messages from the SQS queue", async () => { + const testUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue" + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = testUrl + + // Mock SQS response with messages + const mockMessages = [ + { + MessageId: "1", + Body: JSON.stringify({PrescriptionID: "presc1"}), + Attributes: {MessageDeduplicationId: "dedup1", MessageGroupId: "group1"} + }, + { + MessageId: "2", + Body: JSON.stringify({PrescriptionID: "presc2"}), + Attributes: {MessageDeduplicationId: "dedup2", MessageGroupId: "group2"} + } + ] + mockSend.mockReturnValueOnce({ + Messages: mockMessages + }) + + const result = await receivePostDatedSQSMessages(logger) + + expect(mockSend).toHaveBeenCalledTimes(1) + const receiveCommand = mockSend.mock.calls[0][0] as { + input: {MessageAttributeNames?: Array; MessageSystemAttributeNames?: Array} + } + expect(receiveCommand.input.MessageSystemAttributeNames).toEqual(["MessageDeduplicationId", "MessageGroupId"]) + expect(receiveCommand.input.MessageAttributeNames).toEqual(["All"]) + expect(result).toHaveLength(2) + expect(result[0].MessageId).toBe("1") + expect(result[0].prescriptionData.PrescriptionID).toBe("presc1") + expect(result[1].MessageId).toBe("2") + expect(result[1].prescriptionData.PrescriptionID).toBe("presc2") + }) + + it("Should return an empty array if no messages are received", async () => { + const testUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue" + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = testUrl + + // Mock SQS response with no messages + mockSend.mockReturnValueOnce({ + Messages: [] + }) + + const result = await receivePostDatedSQSMessages(logger) + + expect(mockSend).toHaveBeenCalledTimes(1) + expect(result).toHaveLength(0) + }) + }) + + describe("forwardSQSMessageToNotificationQueue", () => { + it("should send a matured post-dated message to the notifications queue", async () => { + const notifyUrl = "https://sqs.eu-west-2.amazonaws.com/123456789012/notify" + process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL = notifyUrl + + const message: PostDatedSQSMessage = { + MessageId: "1", + ReceiptHandle: "handle-1", + prescriptionData: createMockPostModifiedDataItem({RequestID: "req-1"}), + Attributes: {MessageDeduplicationId: "dedup1", MessageGroupId: "group1"} + } + + mockSend.mockReturnValueOnce({ + Successful: [{Id: "0", MessageId: "notify-msg-1"}], + Failed: [] + }) + + const result = await forwardSQSMessageToNotificationQueue(logger, message) + + expect(mockSend).toHaveBeenCalledTimes(1) + const command = mockSend.mock.calls[0][0] as {input: {QueueUrl: string; Entries: Array<{MessageBody: string}>}} + expect(command.input.QueueUrl).toBe(notifyUrl) + expect(command.input.Entries[0].MessageBody).toBe(JSON.stringify(message.prescriptionData)) + expect(result).toBe("notify-msg-1") + }) + + it("should throw an error if the deduplication ID is missing", async () => { + process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL = "https://sqs.eu-west-2.amazonaws.com/123456789012/notify" + + const message: PostDatedSQSMessage = { + MessageId: "1", + ReceiptHandle: "handle-1", + prescriptionData: createMockPostModifiedDataItem({RequestID: "req-1"}), + Attributes: {} // Missing MessageDeduplicationId + } + + await expect( + forwardSQSMessageToNotificationQueue(logger, message) + ).rejects.toThrow("Missing MessageDeduplicationId in SQS message attributes") + + expect(mockSend).not.toHaveBeenCalled() + }) + }) + + describe("removeSQSMessage", () => { + it("Should remove a message from the SQS queue", async () => { + const testUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue" + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = testUrl + + const messageToRemove = {MessageId: "1", ReceiptHandle: "handle1"} + + // Mock SQS delete response + mockSend.mockReturnValueOnce({ + Successful: [{Id: messageToRemove.MessageId}], + Failed: [] + }) + + await removeSQSMessage(logger, messageToRemove) + + expect(mockSend).toHaveBeenCalledTimes(1) + expect(infoSpy).toHaveBeenCalledWith("Successfully removed 1 messages from SQS") + }) + + it("Should log errors but not throw if deletion fails", async () => { + // We don't want to throw on failed deletions, as this would cause + // later batches to be skipped unnecessarily. + // The messages that are failed to delete will become visible and be processed again after the visibility timeout + const testUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue" + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = testUrl + + const messageToRemove = {MessageId: "1", ReceiptHandle: "handle1"} + + // Mock SQS delete response with failures + mockSend.mockReturnValueOnce({ + Successful: [], + Failed: [{Id: "1", Message: "Some error"}] + }) + + await removeSQSMessage(logger, messageToRemove) + + expect(mockSend).toHaveBeenCalledTimes(1) + expect(errorSpy).toHaveBeenCalledWith("Some messages failed to delete", { + failed: [{Id: "1", Message: "Some error"}] + }) + }) + }) + + describe("returnMessageToQueue", () => { + it("Should return a message to the SQS queue by updating its visibility timeout", async () => { + const testUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue" + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = testUrl + + const messageToReturn: PostDatedSQSMessage = { + MessageId: "1", + ReceiptHandle: "handle1", + prescriptionData: createMockPostModifiedDataItem({}) + } + + // Mock SQS change visibility response + mockSend.mockReturnValueOnce({ + // No specific return value needed for ChangeMessageVisibilityBatch + }) + + await returnMessageToQueue(logger, messageToReturn) + + expect(mockSend).toHaveBeenCalledTimes(1) + expect(infoSpy).toHaveBeenCalledWith("Returning message to queue with timeouts", { + sqsMessage: messageToReturn, + visibilityTimeout: 300 + }) + expect(errorSpy).not.toHaveBeenCalled() + }) + + it("should log an error if SQS change visibility fails", async () => { + const testUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue" + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = testUrl + + const messageToReturn: PostDatedSQSMessage = { + MessageId: "1", + ReceiptHandle: "handle1", + prescriptionData: createMockPostModifiedDataItem({}) + } + + // Mock SQS change visibility to throw an error + const expectedError = new Error("SQS change visibility failed") + mockSend.mockReturnValueOnce(Promise.reject(expectedError)) + + await returnMessageToQueue(logger, messageToReturn) + + expect(mockSend).toHaveBeenCalledTimes(1) + expect(errorSpy).toHaveBeenCalledWith("SQS change visibility failed", {error: expectedError}) + }) + }) +}) diff --git a/packages/postDatedLambda/tests/testUtils.ts b/packages/postDatedLambda/tests/testUtils.ts new file mode 100644 index 0000000000..2f172efa8b --- /dev/null +++ b/packages/postDatedLambda/tests/testUtils.ts @@ -0,0 +1,13 @@ +import {NotifyDataItem} from "packages/common/commonTypes/lib/src" + +export function createMockPostModifiedDataItem(overrides: Partial): NotifyDataItem { + return { + PatientNHSNumber: "0123456789", + PharmacyODSCode: "ABC123", + RequestID: "x-request-id", + Status: "ready to collect", + TaskID: "mnopqr-ghijkl-abcdef", + PrescriptionID: "abcdef-ghijkl-mnopqr", + ...overrides + } +} diff --git a/packages/postDatedLambda/tsconfig.json b/packages/postDatedLambda/tsconfig.json new file mode 100644 index 0000000000..7e1ad6b35c --- /dev/null +++ b/packages/postDatedLambda/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.defaults.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "lib" + }, + "references": [ + {"path": "../common/testing"} + ], + "include": ["src/**/*", "tests/**/*", "cli/**/*"], + "exclude": ["node_modules"] +} diff --git a/packages/updatePrescriptionStatus/.jest/setEnvVars.js b/packages/updatePrescriptionStatus/.jest/setEnvVars.js index 2547377af2..67d1f3297a 100644 --- a/packages/updatePrescriptionStatus/.jest/setEnvVars.js +++ b/packages/updatePrescriptionStatus/.jest/setEnvVars.js @@ -1,5 +1,7 @@ /* eslint-disable no-undef */ process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL = "dummy_notify_sqs"; +process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = "dummy_postdated_sqs"; +process.env.ENABLE_POST_DATED_NOTIFICATIONS = "true"; process.env.AWS_REGION = "eu-west-2"; process.env.SQS_SALT = "the quick brown fox something something" process.env.ENABLED_SITE_ODS_CODES_PARAM = "ENABLED_SITE_ODS_CODES_PARAM" diff --git a/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts b/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts index 5b2d8e200f..a9d72587ea 100644 --- a/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts +++ b/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts @@ -381,11 +381,11 @@ export function buildDataItems( TaskID: task.id!, TerminalStatus: task.status, ApplicationName: applicationName, - ExpiryTime: (Math.floor(+new Date() / 1000) + TTL_DELTA) + ExpiryTime: (Math.floor(Date.now() / 1000) + TTL_DELTA) } if (task.meta?.lastUpdated) { - (dataItem as any).PostDatedLastModifiedSetAt = task.meta.lastUpdated + dataItem.PostDatedLastModifiedSetAt = task.meta.lastUpdated } dataItems.push(dataItem) @@ -413,7 +413,7 @@ async function logTransitions(dataItems: Array): Promis if (previousItem) { const newDate = new Date(currentItem.LastModified) const previousDate = new Date(previousItem.LastModified) - logger.info("Transitioning item status.", { + logger.info(LOG_MESSAGES.PSU0001, { prescriptionID: currentItem.PrescriptionID, lineItemID: currentItem.LineItemID, nhsNumber: currentItem.PatientNHSNumber, @@ -424,7 +424,8 @@ async function logTransitions(dataItems: Array): Promis newStatus: currentItem.Status, previousStatus: previousItem.Status, newTerminalStatus: currentItem.TerminalStatus, - previousTerminalStatus: previousItem.TerminalStatus + previousTerminalStatus: previousItem.TerminalStatus, + isPostDated: currentItem.PostDatedLastModifiedSetAt }) } } catch (e) { diff --git a/packages/updatePrescriptionStatus/src/utils/sqsClient.ts b/packages/updatePrescriptionStatus/src/utils/sqsClient.ts index 2bde5b44e3..aff5bcfbbe 100644 --- a/packages/updatePrescriptionStatus/src/utils/sqsClient.ts +++ b/packages/updatePrescriptionStatus/src/utils/sqsClient.ts @@ -1,14 +1,19 @@ import {Logger} from "@aws-lambda-powertools/logger" -import {SQSClient, SendMessageBatchCommand} from "@aws-sdk/client-sqs" +import {SQSClient, SendMessageBatchCommand, SendMessageBatchRequestEntry} from "@aws-sdk/client-sqs" import {getSecret} from "@aws-lambda-powertools/parameters/secrets" -import {createHmac} from "crypto" +import {createHmac} from "node:crypto" -import {NotifyDataItem, PSUDataItemWithPrevious} from "@psu-common/commonTypes" +import {NotifyDataItem, PSUDataItem, PSUDataItemWithPrevious} from "@psu-common/commonTypes" import {checkSiteOrSystemIsNotifyEnabled} from "../validation/notificationSiteAndSystemFilters" +// eslint-disable-next-line max-len +const ENABLE_POST_DATED_NOTIFICATIONS = (process.env.ENABLE_POST_DATED_NOTIFICATIONS || "false").toLowerCase() === "true" + const sqsUrl: string | undefined = process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL +const postDatedSqsUrl: string | undefined = process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL + const fallbackSalt = "DEV SALT" // The AWS_REGION is always defined in lambda environments @@ -29,6 +34,93 @@ function chunkArray(arr: Array, size: number): Array> { return chunks } +function buildSqsBatchEntries( + items: Array, + requestId: string, + sqsSalt: string +): Array { + return items.map((item, idx) => ({ + Id: idx.toString(), + MessageBody: JSON.stringify(item as NotifyDataItem), + MessageDeduplicationId: saltedHash(`${item.PatientNHSNumber}:${item.PharmacyODSCode}`, sqsSalt), + MessageGroupId: requestId, + MessageAttributes: { + RequestId: { + DataType: "String", + StringValue: requestId + } + } + })) +} + +/** + * Sends entries to the SQS queue in batches of 10 + * @param entries + * @param queueUrl + * @param requestId + * @param logger + * @returns An array of the created MessageIds + */ +async function sendEntriesToQueue( + entries: Array, + queueUrl: string, + requestId: string, + logger: Logger +): Promise> { + if (!entries.length) { + return [] + } + + const batches = chunkArray(entries, 10) + + // Each batch is converted to an array of strings, so we end up with an array of arrays of strings + // (or rather, their promises) + const batchPromises = batches.map(async batch => { + try { + logger.info( + "Pushing a batch of notification requests to SQS", + { + batchLength: batch.length, + deduplicationIds: batch.map(e => e.MessageDeduplicationId), + requestId, + queueUrl + } + ) + + const command = new SendMessageBatchCommand({ + QueueUrl: queueUrl, + Entries: batch + }) + const result = await sqs.send(command) + + let successfulIds: Array = [] + if (result.Successful?.length) { + logger.info("Successfully sent a batch of prescriptions to the SQS", {result, queueUrl}) + + // For each successful message, get its message ID. I don't think there will ever be undefined + // actually in here, but the typing suggests that there could be so filter those out + successfulIds = result.Successful + .map(e => e.MessageId) + .filter((msgId): msgId is string => msgId !== undefined) + } + + // Some may succeed, and some may fail. So check for both + if (result.Failed?.length) { + throw new Error(`Failed to send a batch of prescriptions to the SQS ${queueUrl}`) + } + + return successfulIds + } catch (error) { + logger.error("Failed to send a batch of prescriptions to the SQS", {error, queueUrl}) + throw error + } + }) + + // Flatten the array of arrays of strings into a single array of strings + const batchResults = Promise.all(batchPromises).then(results => results.flat()) + return batchResults +} + /** * Salts and hashes a string. * @@ -59,6 +151,7 @@ export async function getSaltValue(logger: Logger): Promise { try { // grab the secret, expecting JSON like { "salt": "string" } const secretJson = await getSecret(process.env.SQS_SALT, {transform: "json"}) + logger.info("Fetched SQS_SALT from Secrets Manager", {secretJson}) // must be a non‐null object with a string .salt if ( @@ -90,9 +183,15 @@ export async function getSaltValue(logger: Logger): Promise { return sqsSalt } +function norm(str: string) { + return str.toLowerCase().trim() +} + /** * Pushes an array of PSUDataItem to the notifications SQS queue * Uses SendMessageBatch to send up to 10 at a time + * Contains the logic for filtering which items should be sent, based on + * which sites/systems are enabled, and which statuses are to be sent * * @param requestId - The x-request-id header from the incoming event * @param data - Array of PSUDataItem to send to SQS @@ -115,21 +214,15 @@ export async function pushPrescriptionToNotificationSQS( // Only allow through sites and systems that are allowedSitesAndSystems const allowedSitesAndSystemsData = await checkSiteOrSystemIsNotifyEnabled(data) - function norm(str: string) { - return str.toLowerCase().trim() - } - // Only these statuses will be pushed to the SQS - const updateStatuses: Array = [ + const updateStatuses: Set = new Set([ norm("ready to collect"), norm("ready to collect - partial") - ] - // Salt for the deduplication hash - const sqsSalt = await getSaltValue(logger) + ]) // Get only items which have the correct current statuses const candidates = allowedSitesAndSystemsData.filter( - (item) => updateStatuses.includes(norm(item.current.Status)) + (item) => updateStatuses.has(norm(item.current.Status)) ) // we don't want items that have gone from "ready to collect" to "ready to collect" @@ -137,77 +230,69 @@ export async function pushPrescriptionToNotificationSQS( const changedStatus = candidates .filter(({current, previous}) => { if (!previous) return true // no previous item (or hit an error getting one) -> treat as changed + if (previous.PostDatedLastModifiedSetAt) return true // previous was post-dated -> treat as changed + if (current.PostDatedLastModifiedSetAt) return true // current is post-dated -> treat as changed return norm(current.Status) !== norm(previous.Status) }) .map(({current}) => current) - // Build SQS batch entries with FIFO parameters - const allEntries = changedStatus - .map((item, idx) => ({ - Id: idx.toString(), - // Only post the required information to SQS - MessageBody: JSON.stringify(item as NotifyDataItem), - // FIFO - // We dedupe on both nhs number and ods code - MessageDeduplicationId: saltedHash(`${item.PatientNHSNumber}:${item.PharmacyODSCode}`, sqsSalt), - MessageGroupId: requestId, - MessageAttributes: { - RequestId: { - DataType: "String", - StringValue: requestId - } - } - })) + let sqsPromises: Promise> + if (ENABLE_POST_DATED_NOTIFICATIONS) { + logger.info("Post-dated notifications are enabled, separating post-dated and non-post-dated items") - if (!allEntries.length) { - // Carry on if we have no updates to make. - logger.info("No entries to post to the notifications SQS") - return [] + if (!postDatedSqsUrl) { + logger.warn("Post-dated Notifications SQS URL not found in environment variables") + throw new Error("Post-dated Notifications SQS URL not configured") + } + + // Build two arrays, one of all post dated, and one of all non-post-dated + const postDatedItems = changedStatus.filter(item => item.PostDatedLastModifiedSetAt) + const nonPostDatedItems = changedStatus.filter(item => !item.PostDatedLastModifiedSetAt) + + const postDatedMessageIds = sendItemsToSQS(postDatedItems, postDatedSqsUrl, requestId, logger) + const nonPostDatedMessageIds = sendItemsToSQS(nonPostDatedItems, sqsUrl, requestId, logger) + sqsPromises = Promise.all([postDatedMessageIds, nonPostDatedMessageIds]).then(results => results.flat()) + } else { + logger.info("Post-dated notifications are disabled, sending all items to the standard notifications queue") + sqsPromises = sendItemsToSQS(changedStatus, sqsUrl, requestId, logger) } logger.info( "The following patients will have prescription update app notifications requested", - {nhsNumbers: allowedSitesAndSystemsData.map(e => e.current.PatientNHSNumber)} + {nhsNumbers: changedStatus.map(e => e.PatientNHSNumber)} ) - // SQS batch calls are limited to 10 messages per request, so chunk the data - const batches = chunkArray(allEntries, 10) + return sqsPromises +} - // Used for the return value - let out: Array = [] +/** + * + * @param items + * @param sqsUrl + * @param requestId + * @param logger + * @returns an array of the sent MessageIDs + */ +async function sendItemsToSQS( + items: Array, + sqsUrl: string, + requestId: string, + logger: Logger +): Promise> { + if (items.length === 0) { + logger.info("No items to send to SQS", {sqsUrl}) + return [] + } - for (const batch of batches) { - try { - logger.info( - "Pushing a batch of notification requests to SQS", - { - batchLength: batch.length, - deduplicationIds: batch.map(e => e.MessageDeduplicationId), - requestId - } - ) + logger.info(`Placing ${items.length} entries into the SQS queue`, {sqsUrl}) - const command = new SendMessageBatchCommand({ - QueueUrl: sqsUrl, - Entries: batch - }) - const result = await sqs.send(command) - if (result.Successful?.length) { - logger.info("Successfully sent a batch of prescriptions to the notifications SQS", {result}) + const sqsSalt = await getSaltValue(logger) - // For each successful message, get its message ID. I don't think there will ever be undefined - // actually in here, but the typing suggests that there could be so filter those out - out.push(...result.Successful.map(e => e.MessageId).filter(msg_id => msg_id !== undefined)) - } - // Some may succeed, and some may fail. So check for both - if (result.Failed?.length) { - throw new Error("Failed to send a batch of prescriptions to the notifications SQS") - } - } catch (error) { - logger.error("Failed to send a batch of prescriptions to the notifications SQS", {error}) - throw error - } - } + const entries = buildSqsBatchEntries( + items, + requestId, + sqsSalt + ) - return out + return sendEntriesToQueue(entries, sqsUrl, requestId, logger) } diff --git a/packages/updatePrescriptionStatus/tests/testSqsClient.test.ts b/packages/updatePrescriptionStatus/tests/testSqsClient.test.ts index 46f73e3815..f720533a4e 100644 --- a/packages/updatePrescriptionStatus/tests/testSqsClient.test.ts +++ b/packages/updatePrescriptionStatus/tests/testSqsClient.test.ts @@ -86,6 +86,20 @@ describe("Unit tests for pushPrescriptionToNotificationSQS", () => { expect(mockSend).not.toHaveBeenCalled() }) + it("throws if the post-dated SQS URL is not configured", async () => { + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL = undefined + const {pushPrescriptionToNotificationSQS: tempFunc} = await import("../src/utils/sqsClient") + + await expect( + tempFunc("req-123", [], logger) + ).rejects.toThrow("Post-dated Notifications SQS URL not configured") + + expect(warnSpy).toHaveBeenCalledWith( + "Post-dated Notifications SQS URL not found in environment variables" + ) + expect(mockSend).not.toHaveBeenCalled() + }) + it("does nothing when there are no eligible statuses", async () => { const data = [ { @@ -114,6 +128,25 @@ describe("Unit tests for pushPrescriptionToNotificationSQS", () => { expect(mockSend).not.toHaveBeenCalled() }) + it("filters out ready to collect items whose status has not changed", async () => { + const data = [ + { + current: createMockDataItem({Status: "ready to collect"}), + previous: createMockDataItem({Status: "ready to collect"}) + }, + { + current: createMockDataItem({Status: "READY TO COLLECT - PARTIAL"}), + previous: createMockDataItem({Status: "ready to collect - partial"}) + } + ] + + await expect( + pushPrescriptionToNotificationSQS("req-no-change", data, logger) + ).resolves.toEqual([]) + + expect(mockSend).not.toHaveBeenCalled() + }) + it("sends only 'ready to collect' messages and succeeds", async () => { const payload = [ { @@ -143,7 +176,7 @@ describe("Unit tests for pushPrescriptionToNotificationSQS", () => { const sent = mockSend.mock.calls[0][0] expect(sent).toBeInstanceOf(SendMessageBatchCommand) if (!(sent instanceof SendMessageBatchCommand)) { - throw new Error("Expected a SendMessageBatchCommand") + throw new TypeError("Expected a SendMessageBatchCommand") } const entries = sent.input.Entries! @@ -163,8 +196,55 @@ describe("Unit tests for pushPrescriptionToNotificationSQS", () => { }) expect(infoSpy).toHaveBeenCalledWith( - "Successfully sent a batch of prescriptions to the notifications SQS", - {result: {Successful: [{}]}} + "Successfully sent a batch of prescriptions to the SQS", + {result: {Successful: [{}]}, queueUrl: process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL} + ) + }) + + it("routes post-dated and standard notifications to their respective queues", async () => { + const postDatedCurrent = createMockDataItem({ + Status: "ready to collect", + PatientNHSNumber: "9999999999", + PharmacyODSCode: "JIM123", + PostDatedLastModifiedSetAt: "2100-01-01T00:00:00Z" + }) + const standardCurrent = createMockDataItem({ + Status: "ready to collect - partial", + PatientNHSNumber: "8888888888", + PharmacyODSCode: "JIM123" + }) + const payload = [ + {previous: createMockDataItem({Status: "previous status"}), current: postDatedCurrent}, + {previous: createMockDataItem({Status: "previous status"}), current: standardCurrent} + ] + + mockSend + .mockImplementationOnce(() => Promise.resolve({Successful: [{MessageId: "pd-id"}]})) + .mockImplementationOnce(() => Promise.resolve({Successful: [{MessageId: "std-id"}]})) + + const result = await pushPrescriptionToNotificationSQS("req-mixed", payload, logger) + + expect(result).toEqual(["pd-id", "std-id"]) // Both have been pushed to SQS, so we get their IDs + expect(mockSend).toHaveBeenCalledTimes(2) + + // Check that the send command was called twice, once with each SQS URL + const queueUrls = mockSend.mock.calls.map(call => { + const command = call[0] + expect(command).toBeInstanceOf(SendMessageBatchCommand) + if (!(command instanceof SendMessageBatchCommand)) { + throw new TypeError("Expected a SendMessageBatchCommand") + } + command.input.Entries!.forEach(entry => { + expect(entry.MessageGroupId).toBe("req-mixed") + }) + return command.input.QueueUrl + }) + + expect(queueUrls).toEqual( + expect.arrayContaining([ + process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL, + process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL + ]) ) }) @@ -184,8 +264,71 @@ describe("Unit tests for pushPrescriptionToNotificationSQS", () => { ).rejects.toThrow(testError) expect(errorSpy).toHaveBeenCalledWith( - "Failed to send a batch of prescriptions to the notifications SQS", - {error: testError} + "Failed to send a batch of prescriptions to the SQS", + {error: testError, queueUrl: process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL} + ) + }) + + it("rejects when the standard queue fails but the post-dated queue succeeds", async () => { + const payload = [ + { + previous: createMockDataItem({Status: "previous status"}), + current: createMockDataItem({ + Status: "ready to collect", + PatientNHSNumber: "444", + PharmacyODSCode: "DDD", + PostDatedLastModifiedSetAt: "2025-05-01T00:00:00Z" + }) + }, + { + previous: createMockDataItem({Status: "previous status"}), + current: createMockDataItem({Status: "ready to collect", PatientNHSNumber: "555", PharmacyODSCode: "EEE"}) + } + ] + const standardQueueError = new Error("Standard queue failure") + + mockSend + .mockImplementationOnce(() => Promise.resolve({Successful: [{MessageId: "pd-ok"}]})) + .mockImplementationOnce(() => Promise.reject(standardQueueError)) + + await expect( + pushPrescriptionToNotificationSQS("req-failure", payload, logger) + ).rejects.toThrow(standardQueueError) + + expect(errorSpy).toHaveBeenCalledWith( + "Failed to send a batch of prescriptions to the SQS", + {error: standardQueueError, queueUrl: process.env.NHS_NOTIFY_PRESCRIPTIONS_SQS_QUEUE_URL} + ) + }) + + it("Rejects when the post-dated queue fails but the standard queue succeeds", async () => { + const payload = [ + { + previous: createMockDataItem({Status: "previous status"}), + current: createMockDataItem({ + Status: "ready to collect", + PatientNHSNumber: "777", + PharmacyODSCode: "GGG", + PostDatedLastModifiedSetAt: "2100-12-12T00:00:00Z" + }) + }, + { + previous: createMockDataItem({Status: "previous status"}), + current: createMockDataItem({Status: "ready to collect", PatientNHSNumber: "888", PharmacyODSCode: "HHH"}) + } + ] + const postDatedQueueError = new Error("Post-dated queue failure") + + mockSend + .mockImplementationOnce(() => Promise.reject(postDatedQueueError)) + .mockImplementationOnce(() => Promise.resolve({Successful: [{MessageId: "std-ok"}]})) + + await expect( + pushPrescriptionToNotificationSQS("req-failure-2", payload, logger) + ).rejects.toThrow(postDatedQueueError) + expect(errorSpy).toHaveBeenCalledWith( + "Failed to send a batch of prescriptions to the SQS", + {error: postDatedQueueError, queueUrl: process.env.POST_DATED_PRESCRIPTIONS_SQS_QUEUE_URL} ) }) @@ -195,12 +338,11 @@ describe("Unit tests for pushPrescriptionToNotificationSQS", () => { previous: createMockDataItem({Status: "previous status"}), current: createMockDataItem({Status: "ready to collect"}) } - } - ) + }) mockSend - .mockImplementationOnce(() => Promise.resolve({Successful: Array(10).fill({})})) - .mockImplementationOnce(() => Promise.resolve({Successful: Array(2).fill({})})) + .mockImplementationOnce(() => Promise.resolve({Successful: new Array(10).fill({})})) + .mockImplementationOnce(() => Promise.resolve({Successful: new Array(2).fill({})})) await pushPrescriptionToNotificationSQS("req-111", payload, logger) expect(mockSend).toHaveBeenCalledTimes(2) @@ -208,10 +350,18 @@ describe("Unit tests for pushPrescriptionToNotificationSQS", () => { it("Uses the fallback salt value but logs a warning about it", async () => { mockGetSecret.mockImplementationOnce(async () => { - return "DEV SALT" + return {"salt": "DEV SALT"} + }) + + const payload = Array.from({length: 1}, () => { + return { + previous: createMockDataItem({Status: "previous status"}), + current: createMockDataItem({Status: "ready to collect"}) + } }) + mockSend.mockImplementationOnce(() => Promise.resolve({Successful: new Array(2).fill({})})) - await pushPrescriptionToNotificationSQS("req-123", [], logger) + await pushPrescriptionToNotificationSQS("req-123", payload, logger) expect(warnSpy) .toHaveBeenCalledWith( diff --git a/sonar-project.properties b/sonar-project.properties index c323354646..815621e84a 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -53,6 +53,10 @@ nhsNotifyLambda.sonar.projectBaseDir=packages/nhsNotifyLambda nhsNotifyLambda.sonar.sources=. nhsNotifyLambda.sonar.javascript.lcov.reportPaths=coverage/lcov.info +postDatedLambda.sonar.projectBaseDir=packages/postDatedLambda +postDatedLambda.sonar.sources=. +postDatedLambda.sonar.javascript.lcov.reportPaths=coverage/lcov.info + nhsNotifyUpdateCallback.sonar.projectBaseDir=packages/nhsNotifyUpdateCallback nhsNotifyUpdateCallback.sonar.sources=. nhsNotifyUpdateCallback.sonar.javascript.lcov.reportPaths=coverage/lcov.info diff --git a/tsconfig.build.json b/tsconfig.build.json index decbcf115a..479e9c0313 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,18 +4,47 @@ "files": [], // Building this project will build all of the following: "references": [ - {"path": "packages/common/utilities"}, - {"path": "packages/common/testing"}, - {"path": "packages/common/middyErrorHandler"}, - {"path": "packages/common/commonTypes"}, - {"path": "packages/gsul"}, - {"path": "packages/updatePrescriptionStatus"}, - {"path": "packages/nhsd-psu-sandbox"}, - {"path": "packages/statusLambda"}, - {"path": "packages/capabilityStatement"}, - {"path": "packages/cpsuLambda"}, - {"path": "packages/checkPrescriptionStatusUpdates"}, - {"path": "packages/nhsNotifyLambda"}, - {"path": "packages/nhsNotifyUpdateCallback"} + { + "path": "packages/common/utilities" + }, + { + "path": "packages/common/testing" + }, + { + "path": "packages/common/middyErrorHandler" + }, + { + "path": "packages/common/commonTypes" + }, + { + "path": "packages/gsul" + }, + { + "path": "packages/updatePrescriptionStatus" + }, + { + "path": "packages/nhsd-psu-sandbox" + }, + { + "path": "packages/statusLambda" + }, + { + "path": "packages/capabilityStatement" + }, + { + "path": "packages/cpsuLambda" + }, + { + "path": "packages/checkPrescriptionStatusUpdates" + }, + { + "path": "packages/nhsNotifyLambda" + }, + { + "path": "packages/postDatedLambda" + }, + { + "path": "packages/nhsNotifyUpdateCallback" + } ] }