From e7a14912b9b549b939a714e04ae435230a3f4af6 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Wed, 27 Mar 2024 12:54:44 +0000 Subject: [PATCH 01/33] add gsul --- ...scription-status-update-api.code-workspace | 4 + Makefile | 1 + SAMtemplates/main_template.yaml | 73 +-- package-lock.json | 492 +++++++++++++++++- package.json | 3 +- packages/gsul/.eslintrc | 39 ++ packages/gsul/package.json | 32 ++ packages/gsul/src/errorHandler.ts | 53 ++ packages/gsul/src/getStatusUpdates.ts | 147 ++++++ packages/gsul/src/schema/request.ts | 32 ++ packages/gsul/src/schema/response.ts | 58 +++ packages/gsul/tests/testGetStatusUpdates.ts | 0 packages/gsul/tsconfig.json | 11 + .../src/updatePrescriptionStatus.ts | 6 +- .../tests/test-handler.test.ts | 46 +- 15 files changed, 949 insertions(+), 48 deletions(-) create mode 100644 packages/gsul/.eslintrc create mode 100644 packages/gsul/package.json create mode 100644 packages/gsul/src/errorHandler.ts create mode 100644 packages/gsul/src/getStatusUpdates.ts create mode 100644 packages/gsul/src/schema/request.ts create mode 100644 packages/gsul/src/schema/response.ts create mode 100644 packages/gsul/tests/testGetStatusUpdates.ts create mode 100644 packages/gsul/tsconfig.json diff --git a/.vscode/eps-prescription-status-update-api.code-workspace b/.vscode/eps-prescription-status-update-api.code-workspace index 716b84409..14ba06779 100644 --- a/.vscode/eps-prescription-status-update-api.code-workspace +++ b/.vscode/eps-prescription-status-update-api.code-workspace @@ -11,6 +11,10 @@ { "name": "packages/updatePrescriptionStatus", "path": "../packages/updatePrescriptionStatus" + }, + { + "name": "packages/gsul", + "path": "../packages/gsul" } ], "settings": { diff --git a/Makefile b/Makefile index 598a95d77..68d17f247 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,7 @@ compile: compile-node lint-node: compile-node npm run lint --workspace packages/specification npm run lint --workspace packages/updatePrescriptionStatus + npm run lint --workspace packages/gsul lint-samtemplates: poetry run cfn-lint -t SAMtemplates/*.yaml diff --git a/SAMtemplates/main_template.yaml b/SAMtemplates/main_template.yaml index 99bcbc089..32f2d53c6 100644 --- a/SAMtemplates/main_template.yaml +++ b/SAMtemplates/main_template.yaml @@ -115,6 +115,40 @@ Resources: tsconfig: updatePrescriptionStatus/tsconfig.json EntryPoints: - updatePrescriptionStatus/src/updatePrescriptionStatus.ts + + # updatePrescriptionStatus lambda + GSULResources: + Type: AWS::Serverless::Application + Properties: + Location: lambda_resources.yaml + Parameters: + CloudWatchKMSKey: !ImportValue account-resources:CloudwatchLogsKmsKeyArn + SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole + SplunkDeliveryStream: !ImportValue lambda-resources:SplunkDeliveryStream + EnableSplunk: !Ref EnableSplunk + LambdaName: !Sub "${AWS::StackName}-GSUL" + LogRetentionDays: !Ref LogRetentionDays + GSUL: + Type: AWS::Serverless::Function + Properties: + FunctionName: !Sub "${AWS::StackName}-GSUL" + CodeUri: ../packages + Handler: "getStatusUpdates.handler" + Role: !GetAtt GSULResources.Outputs.LambdaRoleArn + Environment: + Variables: + TABLE_NAME: !Sub "${AWS::StackName}-PrescriptionStatusUpdates" + LOG_LEVEL: !Ref LogLevel + Metadata: # Manage esbuild properties + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: "es2020" + Sourcemap: true + tsconfig: gsul/tsconfig.json + EntryPoints: + - gsul/src/getStatusUpdates.ts + # Prescription Status Updates table PrescriptionStatusUpdatesTable: Type: "AWS::DynamoDB::Table" @@ -138,38 +172,19 @@ Resources: ReadCapacityUnits: "5" WriteCapacityUnits: "5" GlobalSecondaryIndexes: - - IndexName: "RequestIDIndex" - KeySchema: - - AttributeName: "RequestID" - KeyType: "HASH" - - AttributeName: "PrescriptionID" - KeyType: "RANGE" - Projection: - NonKeyAttributes: - - "PatientNHSNumber" - - "PharmacyODSCode" - - "TaskID" - - "LineItemID" - - "TerminalStatus" - - "RequestMessage" - ProjectionType: "INCLUDE" - ProvisionedThroughput: - ReadCapacityUnits: "5" - WriteCapacityUnits: "5" - IndexName: "PrescriptionIDIndex" KeySchema: - AttributeName: "PrescriptionID" KeyType: "HASH" - - AttributeName: "PatientNHSNumber" + - AttributeName: "PharmacyODSCode" KeyType: "RANGE" Projection: NonKeyAttributes: - - "PharmacyODSCode" - - "TaskID" - "LineItemID" - "TerminalStatus" - - "RequestID" - - "RequestMessage" + - "LastModified" + - "Status" + - "PatientNHSNumber" ProjectionType: "INCLUDE" ProvisionedThroughput: ReadCapacityUnits: "5" @@ -183,11 +198,10 @@ Resources: Projection: NonKeyAttributes: - "PharmacyODSCode" - - "TaskID" - "LineItemID" - "TerminalStatus" - - "RequestID" - - "RequestMessage" + - "LastModified" + - "Status" ProjectionType: "INCLUDE" ProvisionedThroughput: ReadCapacityUnits: "5" @@ -201,11 +215,10 @@ Resources: Projection: NonKeyAttributes: - "PatientNHSNumber" - - "TaskID" - "LineItemID" - "TerminalStatus" - - "RequestID" - - "RequestMessage" + - "LastModified" + - "Status" ProjectionType: "INCLUDE" ProvisionedThroughput: ReadCapacityUnits: "5" @@ -214,7 +227,7 @@ Resources: Type: AWS::IAM::ManagedPolicy Properties: Roles: - - !GetAtt UpdatePrescriptionStatusResources.Outputs.LambdaRoleName + - !GetAtt GSULResources.Outputs.LambdaRoleName PolicyDocument: Version: "2012-10-17" Statement: diff --git a/package-lock.json b/package-lock.json index 2410eb84e..85dc107a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "MIT", "workspaces": [ "packages/specification", - "packages/updatePrescriptionStatus" + "packages/updatePrescriptionStatus", + "packages/gsul" ], "dependencies": { "esbuild": "^0.20.1" @@ -718,6 +719,23 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/lib-dynamodb": { + "version": "3.540.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.540.0.tgz", + "integrity": "sha512-zOzYsfzL7r403A6jriB3/KrlCvXO/jErqNMIE9EqEGloC6XVd64aKYo6qO8+LQbXNQ67hje4Wh/5wDYADC15mw==", + "dependencies": { + "@aws-sdk/util-dynamodb": "3.540.0", + "@smithy/smithy-client": "^2.5.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.0.0" + } + }, "node_modules/@aws-sdk/middleware-endpoint-discovery": { "version": "3.535.0", "license": "Apache-2.0", @@ -1448,6 +1466,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.24.0", "dev": true, @@ -1648,6 +1677,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fluent/syntax": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@fluent/syntax/-/syntax-0.18.1.tgz", + "integrity": "sha512-h0dnIoIg1RwUZRKynTizlVCe0v5qAO9d92ugmRi8sVMM15mwwftf7dpEKdDRc/pybbrpDhmvZRykEel0Cs/RYg==", + "engines": { + "node": ">=12.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "dev": true, @@ -2113,6 +2151,79 @@ "url": "https://github.com/sponsors/willfarrell" } }, + "node_modules/@middy/util": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@middy/util/-/util-5.3.2.tgz", + "integrity": "sha512-/Y5xxPwkQAigphYmJhThN6TGQCCFgybOYYK7SfiEr8cY7IhYnTys2uyTJR7ziS5jP82zNlgE9H5ZEj6IswHOKQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, + "node_modules/@middy/validator": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/@middy/validator/-/validator-5.3.2.tgz", + "integrity": "sha512-fI1s7gFoYP8144b41VZOE0cJQjMpoNGydSHPoNMeTbvNedM8uHhxeIJR8fVi9LPweGzh3GdzGMGttglHuZsy5w==", + "dependencies": { + "@middy/util": "5.3.2", + "ajv": "8.12.0", + "ajv-errors": "3.0.0", + "ajv-formats": "2.1.1", + "ajv-formats-draft2019": "1.6.1", + "ajv-ftl-i18n": "0.1.1", + "ajv-keywords": "5.1.0", + "fast-uri": "2.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, + "node_modules/@middy/validator/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@middy/validator/node_modules/ajv-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/@middy/validator/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@middy/validator/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==" + }, "node_modules/@nhs/fhir-middy-error-handler": { "version": "2.0.0", "license": "MIT", @@ -3780,7 +3891,6 @@ }, "node_modules/ajv": { "version": "6.12.6", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -3793,6 +3903,80 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats-draft2019": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ajv-formats-draft2019/-/ajv-formats-draft2019-1.6.1.tgz", + "integrity": "sha512-JQPvavpkWDvIsBp2Z33UkYCtXCSpW4HD3tAZ+oL4iEFOk9obQZffx0yANwECt6vzr6ET+7HN5czRyqXbnq/u0Q==", + "dependencies": { + "punycode": "^2.1.1", + "schemes": "^1.4.0", + "smtp-address-parser": "^1.0.3", + "uri-js": "^4.4.1" + }, + "peerDependencies": { + "ajv": "*" + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/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==" + }, + "node_modules/ajv-ftl-i18n": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ajv-ftl-i18n/-/ajv-ftl-i18n-0.1.1.tgz", + "integrity": "sha512-bJwD8xsGqeI3CvLv1lWdJAGaXoEG8PlDqzx1W5U87pWvEhQR+DqnTZsRQpAo736t9haWR06ECtyJA3GTO/Zphw==", + "dependencies": { + "commander": "10.0.0", + "fluent-transpiler": "0.2.1" + }, + "bin": { + "ajv-ftl": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/willfarrell" + } + }, + "node_modules/ajv-ftl-i18n/node_modules/commander": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", + "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "engines": { + "node": ">=14" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "dev": true, @@ -4223,6 +4407,15 @@ "node": ">=6" } }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, "node_modules/camelcase": { "version": "5.3.1", "license": "MIT", @@ -4249,6 +4442,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, "node_modules/chalk": { "version": "4.1.2", "license": "MIT", @@ -4263,6 +4466,25 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/char-regex": { "version": "1.0.2", "dev": true, @@ -4443,6 +4665,16 @@ "proto-list": "~1.2.1" } }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, "node_modules/conventional-changelog-angular": { "version": "7.0.0", "dev": true, @@ -4745,6 +4977,11 @@ "node": ">=8" } }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, "node_modules/doctrine": { "version": "3.0.0", "dev": true, @@ -4756,6 +4993,15 @@ "node": ">=6.0.0" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "dev": true, @@ -5347,6 +5593,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" @@ -5384,7 +5635,6 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -5392,6 +5642,11 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + }, "node_modules/fast-xml-parser": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", @@ -5523,6 +5778,27 @@ "dev": true, "license": "ISC" }, + "node_modules/fluent-transpiler": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fluent-transpiler/-/fluent-transpiler-0.2.1.tgz", + "integrity": "sha512-pdF72/XOg0NHj97E3aXu9k5PI8FmpRRrk4HMDeo9KRm7eoNczBTHnQjQMuqsAJ06ejT9teAA5fL8qUIhtUe6oA==", + "dependencies": { + "@fluent/syntax": "0.18.1", + "change-case": "4.1.2", + "commander": "9.4.0" + }, + "bin": { + "ftl": "cli.js" + } + }, + "node_modules/fluent-transpiler/node_modules/commander": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz", + "integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/for-each": { "version": "0.3.3", "dev": true, @@ -5775,6 +6051,10 @@ "dev": true, "license": "MIT" }, + "node_modules/gsul": { + "resolved": "packages/gsul", + "link": true + }, "node_modules/handlebars": { "version": "4.7.8", "dev": true, @@ -5860,6 +6140,15 @@ "node": ">= 0.4" } }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/highlight.js": { "version": "10.7.3", "dev": true, @@ -6903,9 +7192,20 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.0.1.tgz", + "integrity": "sha512-ANphQxnKbzLWPeYDmdoci8C9g9ttpfMx8etTlJJ8UCEmNXH9jxGkn3AAbMe+lR4N5OG/01nYxPrDyugLdsRt+A==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^1.2.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -7217,6 +7517,14 @@ "dev": true, "license": "MIT" }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "dev": true, @@ -7416,6 +7724,11 @@ "obliterator": "^1.6.1" } }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "node_modules/ms": { "version": "2.1.2", "dev": true, @@ -7436,6 +7749,32 @@ "dev": true, "license": "MIT" }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/neo-async": { "version": "2.6.2", "dev": true, @@ -7466,6 +7805,15 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-emoji": { "version": "2.1.3", "dev": true, @@ -10492,6 +10840,15 @@ "node": ">=6" } }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -10538,6 +10895,24 @@ "dev": true, "license": "MIT" }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/path-exists": { "version": "4.0.0", "license": "MIT", @@ -10885,6 +11260,23 @@ ], "license": "MIT" }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/rc": { "version": "1.2.8", "dev": true, @@ -11124,6 +11516,11 @@ "once": "^1.3.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/registry-auth-token": { "version": "5.0.2", "dev": true, @@ -11204,6 +11601,14 @@ "node": ">=10" } }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "engines": { + "node": ">=0.12" + } + }, "node_modules/reusify": { "version": "1.0.4", "dev": true, @@ -11272,6 +11677,14 @@ "dev": true, "license": "ISC" }, + "node_modules/schemes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/schemes/-/schemes-1.4.0.tgz", + "integrity": "sha512-ImFy9FbCsQlVgnE3TCWmLPCFnVzx0lHL/l+umHplDqAKd0dzFpnS6lFZIpagBlYhKwzVmlV36ec0Y1XTu8JBAQ==", + "dependencies": { + "extend": "^3.0.0" + } + }, "node_modules/semantic-release": { "version": "23.0.6", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-23.0.6.tgz", @@ -11580,6 +11993,16 @@ "dev": true, "license": "ISC" }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "license": "ISC" @@ -11769,6 +12192,26 @@ "node": "*" } }, + "node_modules/smtp-address-parser": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.0.10.tgz", + "integrity": "sha512-Osg9LmvGeAG/hyao4mldbflLOkkr3a+h4m1lwKCK5U8M6ZAr7tdXEz/+/vr752TSGE4MNUlUl9cIK2cB8cgzXg==", + "dependencies": { + "nearley": "^2.20.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map": { "version": "0.6.1", "dev": true, @@ -12231,6 +12674,11 @@ "node": ">=0.6" } }, + "node_modules/ts-algebra": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-1.2.2.tgz", + "integrity": "sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA==" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "dev": true, @@ -12469,6 +12917,22 @@ "resolved": "packages/updatePrescriptionStatus", "link": true }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/uri-js": { "version": "4.4.1", "license": "BSD-2-Clause", @@ -12739,6 +13203,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/gsul": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@aws-lambda-powertools/commons": "^2.0.0", + "@aws-lambda-powertools/logger": "^2.0.0", + "@aws-sdk/client-dynamodb": "^3.540.0", + "@aws-sdk/lib-dynamodb": "^3.540.0", + "@aws-sdk/util-dynamodb": "^3.540.0", + "@middy/core": "^5.2.6", + "@middy/input-output-logger": "^5.2.6", + "@middy/validator": "^5.3.2", + "@nhs/fhir-middy-error-handler": "^2.0.0", + "json-schema-to-ts": "^3.0.1" + }, + "devDependencies": { + "@types/uuid": "^9.0.8", + "aws-sdk-client-mock": "^4.0.0" + } + }, "packages/specification": { "name": "apim-spec", "version": "0.0.1", diff --git a/package.json b/package.json index c01ccfbc6..235bad814 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "license": "MIT", "workspaces": [ "packages/specification", - "packages/updatePrescriptionStatus" + "packages/updatePrescriptionStatus", + "packages/gsul" ], "devDependencies": { "@semantic-release/changelog": "^6.0.3", diff --git a/packages/gsul/.eslintrc b/packages/gsul/.eslintrc new file mode 100644 index 000000000..437314157 --- /dev/null +++ b/packages/gsul/.eslintrc @@ -0,0 +1,39 @@ +{ + "root": true, + "env": { + "node": true + }, + "ignorePatterns": ["**/lib/*"], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint", "import-newlines"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@typescript-eslint/array-type": ["error", {"default": "generic"}], + "@typescript-eslint/consistent-type-assertions": [ + "error", + {"assertionStyle": "as", "objectLiteralTypeAssertions": "never"} + ], + "block-spacing": "error", + "brace-style": ["error", "1tbs"], + "comma-dangle": ["error", "never"], + "comma-spacing": ["error", {"before": false, "after": true}], + "dot-location": ["error", "property"], + "eol-last": ["error", "always"], + "eqeqeq": "error", + "func-call-spacing": "error", + "func-style": ["error", "declaration", {"allowArrowFunctions": true}], + "import-newlines/enforce": ["error", {"items": 3, "max-len": 120, "semi": false}], + "indent": ["error", 2, {"SwitchCase": 1}], + "max-len": ["error", 120], + "no-multi-spaces": "error", + "no-multiple-empty-lines": ["error", {"max": 1}], + "no-trailing-spaces": "error", + "object-curly-spacing": ["error", "never"], + "quotes": ["error", "double", {"allowTemplateLiterals": true, "avoidEscape": true}], + "semi": ["error", "never"] + } +} diff --git a/packages/gsul/package.json b/packages/gsul/package.json new file mode 100644 index 000000000..726ec807f --- /dev/null +++ b/packages/gsul/package.json @@ -0,0 +1,32 @@ +{ + "name": "gsul", + "version": "1.0.0", + "description": "Get status update lambda", + "main": "getStatusUpdates.js", + "author": "NHS Digital", + "license": "MIT", + "type": "module", + "scripts": { + "unit": "POWERTOOLS_DEV=true NODE_OPTIONS=--experimental-vm-modules jest --no-cache --coverage", + "lint": "eslint . --ext .ts --max-warnings 0 --fix", + "compile": "tsc", + "test": "npm run compile && npm run unit", + "check-licenses": "license-checker --failOn GPL --failOn LGPL --start ../.." + }, + "dependencies": { + "@aws-lambda-powertools/commons": "^2.0.0", + "@aws-lambda-powertools/logger": "^2.0.0", + "@aws-sdk/client-dynamodb": "^3.540.0", + "@aws-sdk/lib-dynamodb": "^3.540.0", + "@aws-sdk/util-dynamodb": "^3.540.0", + "@middy/core": "^5.2.6", + "@middy/input-output-logger": "^5.2.6", + "@middy/validator": "^5.3.2", + "@nhs/fhir-middy-error-handler": "^2.0.0", + "json-schema-to-ts": "^3.0.1" + }, + "devDependencies": { + "@types/uuid": "^9.0.8", + "aws-sdk-client-mock": "^4.0.0" + } +} diff --git a/packages/gsul/src/errorHandler.ts b/packages/gsul/src/errorHandler.ts new file mode 100644 index 000000000..edf0896b2 --- /dev/null +++ b/packages/gsul/src/errorHandler.ts @@ -0,0 +1,53 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {MiddlewareObj} from "@middy/core" +import {Logger} from "@aws-lambda-powertools/logger" +import{responseType} from "./schema/response.ts" + +type MockLogger = { + error: (error: Error, message: string) => void +} +type HandlerLogger = Console | MockLogger | Logger +type LoggerAndLevel = { + logger?: HandlerLogger + level?: string +} + +// custom middy error handler to just log the error and return isSuccess = false + +function errorHandler({logger = console, level = "error"}: LoggerAndLevel) { + return { + onError: async (handler) => { + const error: Error | any = handler.error + + // if there are a `statusCode` and an `error` field + // this is a valid http error object + if (typeof logger[level] === "function") { + logger[level]( + { + error: ((e) => ({ + name: e.name, + message: e.message, + stack: e.stack, + details: e.details, + cause: e.cause, + status: e.status, + statusCode: e.statusCode, + expose: e.expose + }))(error) + }, + `${error.name}: ${error.message}` + ) + } + + const responseBody: responseType = { + schemaVersion: 1, + isSuccess: false, + prescriptions: [] + } + + handler.response = responseBody + } + } satisfies MiddlewareObj +} + +export {errorHandler} diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts new file mode 100644 index 000000000..35907d7d5 --- /dev/null +++ b/packages/gsul/src/getStatusUpdates.ts @@ -0,0 +1,147 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {Logger} from "@aws-lambda-powertools/logger" +import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" +import {DynamoDBClient} from "@aws-sdk/client-dynamodb" +import {DynamoDBDocumentClient, QueryCommand, QueryCommandInput} from "@aws-sdk/lib-dynamodb" +import middy from "@middy/core" +import inputOutputLogger from "@middy/input-output-logger" +import validator from "@middy/validator" +import {transpileSchema} from "@middy/validator/transpile" +import {errorHandler} from "./errorHandler.ts" +import{requestSchema, requestType, inputPrescriptionType} from "./schema/request.ts" +import{responseType, outputPrescriptionType, itemType} from "./schema/response.ts" +const logger = new Logger({serviceName: "updatePrescriptionStatus"}) +const client = new DynamoDBClient({region: "eu-west-2"}) +const docClient = DynamoDBDocumentClient.from(client) +const tableName = process.env.TABLE_NAME + +interface DynamoDBResult { + prescriptionID: string | undefined; + itemId: string | undefined; + latestStatus: string | undefined; + isTerminalState: string | undefined; + lastUpdateDateTime: string | undefined +} + +const lambdaHandler = async (event: requestType): Promise => { + + const queryParams = event.prescriptions.map((prescription) => { + // create query for each prescription and ods code passed in + const queryParam : QueryCommandInput = { + TableName: tableName, + IndexName: "PrescriptionIDIndex", + KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", + ExpressionAttributeValues: { + ":inputPharmacyODSCode": prescription.odsCode, + ":inputPrescriptionID": prescription.prescriptionID + } + } + return queryParam + }) + + // run the dynamodb queries + const queryResultsTasks = await runDynamoDBQueries(queryParams) + + // get all the query results + const queryResults = await Promise.all(queryResultsTasks) + + const itemResults = buildResults(event.prescriptions, queryResults) + + const response = { + "schemaVersion": 1, + "isSuccess": true, + "prescriptions": itemResults + } + return response +} + +const runDynamoDBQueries = (queryParams: Array): Array> => { + const queryResultsTasks: Array> = queryParams.map(async (query) => { + // run each query + const command = new QueryCommand(query) + logger.info("running query", {query}) + const dynamoDBresponse = await docClient.send(command) + if (dynamoDBresponse?.Count !== 0) { + // if we have a response get the latest update + const latestUpdate = dynamoDBresponse.Items?.reduce((prev, current) => { + return (prev && Date.parse(prev.LastModified) > Date.parse(current.LastModified)) ? prev : current + }) + + const result: DynamoDBResult = { + prescriptionID: String(latestUpdate?.PrescriptionID), + itemId: String(latestUpdate?.LineItemID), + latestStatus: String(latestUpdate?.Status), + isTerminalState: String(latestUpdate?.TerminalStatus), + lastUpdateDateTime: String(latestUpdate?.LastModified) + } + return Promise.resolve(result) + } + const result: DynamoDBResult = { + prescriptionID: undefined, + itemId: undefined, + latestStatus: undefined, + isTerminalState: undefined, + lastUpdateDateTime: undefined + } + return Promise.resolve(result) + }) + + return queryResultsTasks +} + +const buildResults = (inputPrescriptions: Array, + queryResults: Array): Array => { + // for each prescription id passed in build up a response + const itemResults = inputPrescriptions.map((prescription) => { + const items = queryResults.filter((queryResult) => { + // see if we have any results for the prescription id + return queryResult?.prescriptionID === prescription.prescriptionID + }) + // if we have results then populate the response + if (items.length > 0) { + const responseItems = items.map((item) => { + const returnItem: itemType = { + "itemId": String(item.itemId), + "latestStatus": String(item.latestStatus), + "isTerminalState": String(item.isTerminalState), + "lastUpdateDateTime": String(item.lastUpdateDateTime) + } + return returnItem + }) + const result: outputPrescriptionType = { + "prescriptionID": prescription.prescriptionID, + "onboarded": true, + "items": responseItems + } + return result + } + // we have no results returned so return an empty array of items + const result = { + "prescriptionID": prescription.prescriptionID, + "onboarded": true, + "items": [] + } + return result + }) + return itemResults +} + +export const handler = middy(lambdaHandler) + .use(injectLambdaContext(logger, {clearState: true})) + .use( + inputOutputLogger({ + logger: (request) => { + if (request.response) { + logger.info(request) + } else { + logger.info(request) + } + } + }) + ) + .use(errorHandler({logger: logger})) + .use( + validator({ + eventSchema: transpileSchema(requestSchema) + }) + ) diff --git a/packages/gsul/src/schema/request.ts b/packages/gsul/src/schema/request.ts new file mode 100644 index 000000000..ba3057023 --- /dev/null +++ b/packages/gsul/src/schema/request.ts @@ -0,0 +1,32 @@ +import {FromSchema} from "json-schema-to-ts" + +const inputPrescriptionSchema = { + type: "object", + required: ["prescriptionID", "odsCode"], + properties: { + prescriptionID: { + type: "string" + }, + odsCode: { + type: "string" + } + } +} as const + +const requestSchema = { + type: "object", + required: ["schemaVersion", "prescriptions"], + properties: { + schemaVersion: { + type: "number" + }, + prescriptions: { + type: "array", + items: inputPrescriptionSchema + } + } +} as const + +type requestType = FromSchema +type inputPrescriptionType = FromSchema +export {requestSchema, requestType, inputPrescriptionSchema, inputPrescriptionType} diff --git a/packages/gsul/src/schema/response.ts b/packages/gsul/src/schema/response.ts new file mode 100644 index 000000000..9138197d6 --- /dev/null +++ b/packages/gsul/src/schema/response.ts @@ -0,0 +1,58 @@ +import {FromSchema} from "json-schema-to-ts" + +const itemSchema = { + type: "object", + required: ["itemId", "latestStatus", "isTerminalState", "lastUpdateDateTime"], + properties: { + itemId: { + type: "string" + }, + latestStatus: { + type: "string" + }, + isTerminalState: { + type: "string" + }, + lastUpdateDateTime: { + type: "string" + } + } +} as const + +const outputPrescriptionSchema = { + type: "object", + properties: { + prescriptionID: { + type: "string" + }, + onboarded: { + type: "boolean" + }, + items: { + type: "array", + items: itemSchema + } + } +} as const + +const responseSchema = { + type: "object", + required: ["schemaVersion", "isSuccess", "prescriptions"], + properties: { + schemaVersion: { + type: "number" + }, + isSuccess: { + type: "boolean" + }, + prescriptions: { + type: "array", + items: outputPrescriptionSchema + } + } +} as const + +type responseType = FromSchema +type outputPrescriptionType = FromSchema +type itemType = FromSchema +export {responseSchema, responseType, outputPrescriptionSchema, outputPrescriptionType, itemSchema, itemType} diff --git a/packages/gsul/tests/testGetStatusUpdates.ts b/packages/gsul/tests/testGetStatusUpdates.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/gsul/tsconfig.json b/packages/gsul/tsconfig.json new file mode 100644 index 000000000..81863d8e1 --- /dev/null +++ b/packages/gsul/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.defaults.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "lib", + "allowImportingTsExtensions": true + }, + "references": [], + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules"] +} diff --git a/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts b/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts index bca1aa08b..0fd934584 100644 --- a/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts +++ b/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts @@ -21,6 +21,8 @@ interface DynamoDBItem { LineItemID: string; TerminalStatus: string; RequestMessage: any; + LastModified: string; + Status: string } const lambdaHandler = async (event: APIGatewayProxyEvent): Promise => { @@ -77,7 +79,9 @@ const lambdaHandler = async (event: APIGatewayProxyEvent): Promise { PharmacyODSCode: string, PrescriptionID: string, TaskID: string, - TerminalStatus: string + TerminalStatus: string, + LastModified: string, + Status: string ) => ({ LineItemID: {S: LineItemID}, PatientNHSNumber: {S: PatientNHSNumber}, @@ -64,9 +66,13 @@ describe("Unit test for updatePrescriptionStatus handler", () => { for: {M: {identifier: {M: {value: {S: PatientNHSNumber}}}}}, id: {S: TaskID}, owner: {M: {identifier: {M: {value: {S: PharmacyODSCode}}}}}, - status: {S: TerminalStatus} + status: {S: TerminalStatus}, + lastModified: {S: LastModified}, + businessStatus: {M: {coding: {L: [{M: {display: Status}}]}}} } - } + }, + LastModified: {S: LastModified}, + Status: {S: Status} }) beforeEach(() => { @@ -86,18 +92,20 @@ describe("Unit test for updatePrescriptionStatus handler", () => { owner: {identifier: {value: "PharmacyODSCode"}}, id: "TaskID", focus: {identifier: {value: "LineItemID"}}, - status: "TerminalStatus" + status: "TerminalStatus", + lastModified: "1970-01-01T00:00:00Z", + businessStatus: {coding: [{display: "dispatched"}]} } } ] }) } - jest.spyOn(DynamoDBClient.prototype, "send").mockResolvedValue(undefined as never) + const spy = jest.spyOn(DynamoDBClient.prototype, "send").mockResolvedValue(undefined as never) const result: APIGatewayProxyResult = await handler(event, dummyContext) - expect(DynamoDBClient.prototype.send).toHaveBeenCalledWith( + expect(spy).toHaveBeenCalledWith( expect.objectContaining({ input: expect.objectContaining({ Item: expect.objectContaining( @@ -107,7 +115,9 @@ describe("Unit test for updatePrescriptionStatus handler", () => { "PharmacyODSCode", "PrescriptionID", "TaskID", - "TerminalStatus" + "TerminalStatus", + "1970-01-01T00:00:00Z", + "dispatched" ) ) }) @@ -130,7 +140,10 @@ describe("Unit test for updatePrescriptionStatus handler", () => { owner: {identifier: {value: "PharmacyODSCode1"}}, id: "TaskID1", focus: {identifier: {value: "LineItemID1"}}, - status: "TerminalStatus1" + status: "TerminalStatus1", + lastModified: "1970-01-01T00:00:00Z", + businessStatus: {coding: [{display: "dispatched"}]} + } }, { @@ -140,7 +153,10 @@ describe("Unit test for updatePrescriptionStatus handler", () => { owner: {identifier: {value: "PharmacyODSCode2"}}, id: "TaskID2", focus: {identifier: {value: "LineItemID2"}}, - status: "TerminalStatus2" + status: "TerminalStatus2", + lastModified: "1970-01-01T00:00:00Z", + businessStatus: {coding: [{display: "dispatched"}]} + } } ] @@ -169,7 +185,9 @@ describe("Unit test for updatePrescriptionStatus handler", () => { "PharmacyODSCode1", "PrescriptionID1", "TaskID1", - "TerminalStatus1" + "TerminalStatus1", + "1970-01-01T00:00:00Z", + "dispatched" ) ) }) @@ -186,7 +204,9 @@ describe("Unit test for updatePrescriptionStatus handler", () => { "PharmacyODSCode2", "PrescriptionID2", "TaskID2", - "TerminalStatus2" + "TerminalStatus2", + "1970-01-01T00:00:00Z", + "dispatched" ) ) }) @@ -284,7 +304,9 @@ describe("Unit test for updatePrescriptionStatus handler", () => { owner: {identifier: {value: "PharmacyODSCode"}}, id: "TaskID", focus: {identifier: {value: "LineItemID"}}, - status: "TerminalStatus" + status: "TerminalStatus", + lastModified: "1970-01-01T00:00:00Z", + businessStatus: {coding: [{display: "dispatched"}]} } } ] From 520a228637756e94d387aabfc28acdd1a7a4a301 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Wed, 27 Mar 2024 14:15:40 +0000 Subject: [PATCH 02/33] start testing --- packages/gsul/.vscode/launch.json | 35 +++++++++++++++++ packages/gsul/.vscode/settings.json | 7 ++++ packages/gsul/jest.config.ts | 9 +++++ packages/gsul/jest.debug.config.ts | 9 +++++ packages/gsul/src/getStatusUpdates.ts | 10 +---- packages/gsul/src/schema/result.ts | 9 +++++ .../gsul/tests/testGetStatusUpdates.test.ts | 39 +++++++++++++++++++ packages/gsul/tests/testGetStatusUpdates.ts | 0 packages/gsul/tsconfig.json | 4 +- tsconfig.build.json | 3 +- 10 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 packages/gsul/.vscode/launch.json create mode 100644 packages/gsul/.vscode/settings.json create mode 100644 packages/gsul/jest.config.ts create mode 100644 packages/gsul/jest.debug.config.ts create mode 100644 packages/gsul/src/schema/result.ts create mode 100644 packages/gsul/tests/testGetStatusUpdates.test.ts delete mode 100644 packages/gsul/tests/testGetStatusUpdates.ts diff --git a/packages/gsul/.vscode/launch.json b/packages/gsul/.vscode/launch.json new file mode 100644 index 000000000..8acf7f038 --- /dev/null +++ b/packages/gsul/.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/gsul/.vscode/settings.json b/packages/gsul/.vscode/settings.json new file mode 100644 index 000000000..350126494 --- /dev/null +++ b/packages/gsul/.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/gsul/jest.config.ts b/packages/gsul/jest.config.ts new file mode 100644 index 000000000..fb4d05f26 --- /dev/null +++ b/packages/gsul/jest.config.ts @@ -0,0 +1,9 @@ +import type {JestConfigWithTsJest} from "ts-jest" +import defaultConfig from "../../jest.default.config" + +const jestConfig: JestConfigWithTsJest = { + ...defaultConfig, + "rootDir": "./" +} + +export default jestConfig diff --git a/packages/gsul/jest.debug.config.ts b/packages/gsul/jest.debug.config.ts new file mode 100644 index 000000000..a30627383 --- /dev/null +++ b/packages/gsul/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/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index 35907d7d5..5e80b94eb 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -10,18 +10,12 @@ import {transpileSchema} from "@middy/validator/transpile" import {errorHandler} from "./errorHandler.ts" import{requestSchema, requestType, inputPrescriptionType} from "./schema/request.ts" import{responseType, outputPrescriptionType, itemType} from "./schema/response.ts" +import{DynamoDBResult} from "./schema/result.ts" const logger = new Logger({serviceName: "updatePrescriptionStatus"}) const client = new DynamoDBClient({region: "eu-west-2"}) const docClient = DynamoDBDocumentClient.from(client) const tableName = process.env.TABLE_NAME -interface DynamoDBResult { - prescriptionID: string | undefined; - itemId: string | undefined; - latestStatus: string | undefined; - isTerminalState: string | undefined; - lastUpdateDateTime: string | undefined -} const lambdaHandler = async (event: requestType): Promise => { @@ -89,7 +83,7 @@ const runDynamoDBQueries = (queryParams: Array): Array, +export const buildResults = (inputPrescriptions: Array, queryResults: Array): Array => { // for each prescription id passed in build up a response const itemResults = inputPrescriptions.map((prescription) => { diff --git a/packages/gsul/src/schema/result.ts b/packages/gsul/src/schema/result.ts new file mode 100644 index 000000000..cd195e084 --- /dev/null +++ b/packages/gsul/src/schema/result.ts @@ -0,0 +1,9 @@ +interface DynamoDBResult { + prescriptionID: string | undefined; + itemId: string | undefined; + latestStatus: string | undefined; + isTerminalState: string | undefined; + lastUpdateDateTime: string | undefined + } + +export {DynamoDBResult} diff --git a/packages/gsul/tests/testGetStatusUpdates.test.ts b/packages/gsul/tests/testGetStatusUpdates.test.ts new file mode 100644 index 000000000..45bd2b969 --- /dev/null +++ b/packages/gsul/tests/testGetStatusUpdates.test.ts @@ -0,0 +1,39 @@ +import {assert} from "console" +import {buildResults} from "../src/getStatusUpdates" +import {inputPrescriptionType} from "../src/schema/request" +import {DynamoDBResult} from "../src/schema/result" +import {outputPrescriptionType} from "../src/schema/response" + +describe("Unit tests for build results", () => { + it("should return correct data when a matched prescription found", () => { + const inputPrescriptions: Array = [{ + prescriptionID: "abc", + odsCode: "123" + }] + + const queryResults: Array = [{ + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }] + + const result = buildResults(inputPrescriptions, queryResults) + + const expectedResult: Array = [ + { + prescriptionID: "abc", + onboarded: true, + items: [{ + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }] + } + ] + + assert(result === expectedResult) + }) +}) diff --git a/packages/gsul/tests/testGetStatusUpdates.ts b/packages/gsul/tests/testGetStatusUpdates.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/gsul/tsconfig.json b/packages/gsul/tsconfig.json index 81863d8e1..36cd8ba60 100644 --- a/packages/gsul/tsconfig.json +++ b/packages/gsul/tsconfig.json @@ -3,7 +3,9 @@ "compilerOptions": { "rootDir": ".", "outDir": "lib", - "allowImportingTsExtensions": true + "allowImportingTsExtensions": true, + "noEmit": true, + "strict": false }, "references": [], "include": ["src/**/*", "tests/**/*"], diff --git a/tsconfig.build.json b/tsconfig.build.json index 15c7cf4bb..a19d5841d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -4,6 +4,7 @@ "files": [], // Building this project will build all of the following: "references": [ - { "path": "packages/updatePrescriptionStatus" } + { "path": "packages/updatePrescriptionStatus" }, + { "path": "packages/gsul" } ] } From e4c33d92ffbba894da04405b0e087722a382a74c Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:04:59 +0000 Subject: [PATCH 03/33] more tests --- packages/gsul/src/getStatusUpdates.ts | 63 +++++--- .../gsul/tests/testGetStatusUpdates.test.ts | 142 +++++++++++++++--- 2 files changed, 163 insertions(+), 42 deletions(-) diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index 5e80b94eb..47b83e727 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -9,14 +9,14 @@ import validator from "@middy/validator" import {transpileSchema} from "@middy/validator/transpile" import {errorHandler} from "./errorHandler.ts" import{requestSchema, requestType, inputPrescriptionType} from "./schema/request.ts" -import{responseType, outputPrescriptionType, itemType} from "./schema/response.ts" +import {responseType, outputPrescriptionType, itemType} from "./schema/response.ts" import{DynamoDBResult} from "./schema/result.ts" + const logger = new Logger({serviceName: "updatePrescriptionStatus"}) const client = new DynamoDBClient({region: "eu-west-2"}) const docClient = DynamoDBDocumentClient.from(client) const tableName = process.env.TABLE_NAME - const lambdaHandler = async (event: requestType): Promise => { const queryParams = event.prescriptions.map((prescription) => { @@ -39,7 +39,7 @@ const lambdaHandler = async (event: requestType): Promise => { // get all the query results const queryResults = await Promise.all(queryResultsTasks) - const itemResults = buildResults(event.prescriptions, queryResults) + const itemResults = buildResults(event.prescriptions, queryResults.flat()) const response = { "schemaVersion": 1, @@ -49,35 +49,35 @@ const lambdaHandler = async (event: requestType): Promise => { return response } -const runDynamoDBQueries = (queryParams: Array): Array> => { - const queryResultsTasks: Array> = queryParams.map(async (query) => { +const runDynamoDBQueries = (queryParams: Array): Array>> => { + const queryResultsTasks: Array>> = queryParams.map(async (query) => { // run each query const command = new QueryCommand(query) logger.info("running query", {query}) const dynamoDBresponse = await docClient.send(command) if (dynamoDBresponse?.Count !== 0) { - // if we have a response get the latest update - const latestUpdate = dynamoDBresponse.Items?.reduce((prev, current) => { - return (prev && Date.parse(prev.LastModified) > Date.parse(current.LastModified)) ? prev : current + + const response: Array = dynamoDBresponse.Items?.map((singleUpdate) => { + const result: DynamoDBResult = { + prescriptionID: String(singleUpdate.PrescriptionID), + itemId: String(singleUpdate.LineItemID), + latestStatus: String(singleUpdate.Status), + isTerminalState: String(singleUpdate.TerminalStatus), + lastUpdateDateTime: String(singleUpdate.LastModified) + } + return result }) - const result: DynamoDBResult = { - prescriptionID: String(latestUpdate?.PrescriptionID), - itemId: String(latestUpdate?.LineItemID), - latestStatus: String(latestUpdate?.Status), - isTerminalState: String(latestUpdate?.TerminalStatus), - lastUpdateDateTime: String(latestUpdate?.LastModified) - } - return Promise.resolve(result) + return response } - const result: DynamoDBResult = { + const result: Array = [{ prescriptionID: undefined, itemId: undefined, latestStatus: undefined, isTerminalState: undefined, lastUpdateDateTime: undefined - } - return Promise.resolve(result) + }] + return result }) return queryResultsTasks @@ -93,7 +93,30 @@ export const buildResults = (inputPrescriptions: Array, }) // if we have results then populate the response if (items.length > 0) { - const responseItems = items.map((item) => { + // get the latest update per item id + + // first get the unique item ids + const uniqueItemIds = [...new Set(items.map((item) => { + return item.itemId + }))] + + // now get an array of the latest updates for each unique item id + const latestUpdates = uniqueItemIds.map((itemId)=> { + + // get all the updates for this prescription id and item id + const matchedItems = items.filter((item) => { + return item.prescriptionID === prescription.prescriptionID && item.itemId === itemId + }) + + // now just get the latest update + const latestUpdate = matchedItems.reduce((prev, current) => { + return (prev && Date.parse(prev.lastUpdateDateTime) > Date.parse(current.lastUpdateDateTime)) ? prev : current + }) + + return latestUpdate + }) + + const responseItems = latestUpdates.map((item) => { const returnItem: itemType = { "itemId": String(item.itemId), "latestStatus": String(item.latestStatus), diff --git a/packages/gsul/tests/testGetStatusUpdates.test.ts b/packages/gsul/tests/testGetStatusUpdates.test.ts index 45bd2b969..f727f668a 100644 --- a/packages/gsul/tests/testGetStatusUpdates.test.ts +++ b/packages/gsul/tests/testGetStatusUpdates.test.ts @@ -1,39 +1,137 @@ -import {assert} from "console" import {buildResults} from "../src/getStatusUpdates" import {inputPrescriptionType} from "../src/schema/request" import {DynamoDBResult} from "../src/schema/result" import {outputPrescriptionType} from "../src/schema/response" -describe("Unit tests for build results", () => { - it("should return correct data when a matched prescription found", () => { - const inputPrescriptions: Array = [{ +type scenariosType = { + scenarioDescription: string; + inputPrescriptions: Array; + queryResults: Array, + expectedResult: Array; +} +const scenarios: Array = [ + { + scenarioDescription: "should return correct data when a matched prescription found", + inputPrescriptions: [{ prescriptionID: "abc", odsCode: "123" + }], + queryResults: [{ + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }], + expectedResult: [{ + prescriptionID: "abc", + onboarded: true, + items: [{ + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }] }] - - const queryResults: Array = [{ + }, { + scenarioDescription: "should return no items when empty item status are found", + inputPrescriptions: [{ + prescriptionID: "abc", + odsCode: "123" + }], + queryResults: [], + expectedResult: [{ + prescriptionID: "abc", + onboarded: true, + items: [] + }] + }, { + scenarioDescription: "should return no items when item status are found for different prescription", + inputPrescriptions: [{ prescriptionID: "abc", + odsCode: "123" + }], + queryResults: [{ + prescriptionID: "def", itemId: "item_1", latestStatus: "latest_status", isTerminalState: "is_terminal_status", lastUpdateDateTime: "1970-01-01T00:00:00Z" + }], + expectedResult: [{ + prescriptionID: "abc", + onboarded: true, + items: [] }] - + }, { + scenarioDescription: "should return latest data when a multiple updates found", + inputPrescriptions: [{ + prescriptionID: "abc", + odsCode: "123" + }], + queryResults: [{ + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "early_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }, { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1971-01-01T00:00:00Z" + }], + expectedResult: [{ + prescriptionID: "abc", + onboarded: true, + items: [{ + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1971-01-01T00:00:00Z" + }] + }] + }, { + scenarioDescription: "should return correct data for multiple items", + inputPrescriptions: [{ + prescriptionID: "abc", + odsCode: "123" + }], + queryResults: [{ + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "item_1_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }, { + prescriptionID: "abc", + itemId: "item_2", + latestStatus: "item_2_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1971-01-01T00:00:00Z" + }], + expectedResult: [{ + prescriptionID: "abc", + onboarded: true, + items: [{ + itemId: "item_1", + latestStatus: "item_1_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }, { + itemId: "item_2", + latestStatus: "item_2_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1971-01-01T00:00:00Z" + }] + }] + } +] +describe("Unit tests for buildResults", () => { + it.each(scenarios)("$scenarioDescription", ({inputPrescriptions, queryResults, expectedResult}) => { const result = buildResults(inputPrescriptions, queryResults) - - const expectedResult: Array = [ - { - prescriptionID: "abc", - onboarded: true, - items: [{ - itemId: "item_1", - latestStatus: "latest_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }] - } - ] - - assert(result === expectedResult) + expect(result).toMatchObject(expectedResult) }) + }) From 0f0d7332427436a9f0fd33f4f43e7f9b20249e52 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:17:11 +0000 Subject: [PATCH 04/33] more tests --- package-lock.json | 9 +- packages/gsul/package.json | 4 - packages/gsul/src/dynamoDBclient.ts | 61 +++++++ packages/gsul/src/getStatusUpdates.ts | 47 +----- packages/gsul/src/schema/request.ts | 4 +- packages/gsul/tests/testErrorHandler.test.ts | 67 ++++++++ .../gsul/tests/testGetStatusUpdates.test.ts | 16 +- packages/gsul/tests/testHander.test.ts | 69 ++++++++ .../gsul/tests/testRunDynamoDBQueries.test.ts | 158 ++++++++++++++++++ 9 files changed, 383 insertions(+), 52 deletions(-) create mode 100644 packages/gsul/src/dynamoDBclient.ts create mode 100644 packages/gsul/tests/testErrorHandler.test.ts create mode 100644 packages/gsul/tests/testHander.test.ts create mode 100644 packages/gsul/tests/testRunDynamoDBQueries.test.ts diff --git a/package-lock.json b/package-lock.json index 85dc107a4..1c812e7d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7475,8 +7475,9 @@ }, "node_modules/lodash-es": { "version": "4.17.21", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true }, "node_modules/lodash.capitalize": { "version": "4.2.1", @@ -13217,10 +13218,6 @@ "@middy/validator": "^5.3.2", "@nhs/fhir-middy-error-handler": "^2.0.0", "json-schema-to-ts": "^3.0.1" - }, - "devDependencies": { - "@types/uuid": "^9.0.8", - "aws-sdk-client-mock": "^4.0.0" } }, "packages/specification": { diff --git a/packages/gsul/package.json b/packages/gsul/package.json index 726ec807f..8c0912994 100644 --- a/packages/gsul/package.json +++ b/packages/gsul/package.json @@ -24,9 +24,5 @@ "@middy/validator": "^5.3.2", "@nhs/fhir-middy-error-handler": "^2.0.0", "json-schema-to-ts": "^3.0.1" - }, - "devDependencies": { - "@types/uuid": "^9.0.8", - "aws-sdk-client-mock": "^4.0.0" } } diff --git a/packages/gsul/src/dynamoDBclient.ts b/packages/gsul/src/dynamoDBclient.ts new file mode 100644 index 000000000..7c0677057 --- /dev/null +++ b/packages/gsul/src/dynamoDBclient.ts @@ -0,0 +1,61 @@ + +import {DynamoDBDocumentClient, QueryCommand, QueryCommandInput} from "@aws-sdk/lib-dynamodb" +import {Logger} from "@aws-lambda-powertools/logger" +import{DynamoDBResult} from "./schema/result.ts" +import {NativeAttributeValue} from "@aws-sdk/util-dynamodb" + +export function runDynamoDBQueries (queryParams: Array, + docClient: DynamoDBDocumentClient, + logger: Logger): Array>> { + + // helper function to deal with pagination of results from dynamodb + const getAllData = async (query: QueryCommandInput) => { + const _getAllData = async (query: QueryCommandInput, startKey: Record) => { + if (startKey) { + query.ExclusiveStartKey = startKey + } + const command = new QueryCommand(query) + logger.info("running query", {query}) + return docClient.send(new QueryCommand(query)) + } + let lastEvaluatedKey = null + let rows = [] + do { + const result = await _getAllData(query, lastEvaluatedKey) + rows = rows.concat(result.Items) + lastEvaluatedKey = result.LastEvaluatedKey + } while (lastEvaluatedKey) + return rows + } + + const queryResultsTasks: Array>> = queryParams.map(async (query) => { + // run each query + const items = await getAllData(query) + + if (items.length !== 0) { + + const response: Array = items.map((singleUpdate) => { + const result: DynamoDBResult = { + prescriptionID: String(singleUpdate.PrescriptionID), + itemId: String(singleUpdate.LineItemID), + latestStatus: String(singleUpdate.Status), + isTerminalState: String(singleUpdate.TerminalStatus), + lastUpdateDateTime: String(singleUpdate.LastModified) + } + return result + }) + + return response + } + const result: Array = [{ + prescriptionID: undefined, + itemId: undefined, + latestStatus: undefined, + isTerminalState: undefined, + lastUpdateDateTime: undefined + }] + return result + }) + + return queryResultsTasks +} diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index 47b83e727..8384d4471 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -2,22 +2,25 @@ import {Logger} from "@aws-lambda-powertools/logger" import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" import {DynamoDBClient} from "@aws-sdk/client-dynamodb" -import {DynamoDBDocumentClient, QueryCommand, QueryCommandInput} from "@aws-sdk/lib-dynamodb" +import {DynamoDBDocumentClient, QueryCommandInput} from "@aws-sdk/lib-dynamodb" import middy from "@middy/core" import inputOutputLogger from "@middy/input-output-logger" import validator from "@middy/validator" import {transpileSchema} from "@middy/validator/transpile" import {errorHandler} from "./errorHandler.ts" -import{requestSchema, requestType, inputPrescriptionType} from "./schema/request.ts" +import {runDynamoDBQueries} from "./dynamoDBclient.ts" +import {requestSchema, requestType, inputPrescriptionType} from "./schema/request.ts" import {responseType, outputPrescriptionType, itemType} from "./schema/response.ts" -import{DynamoDBResult} from "./schema/result.ts" +import {DynamoDBResult} from "./schema/result.ts" -const logger = new Logger({serviceName: "updatePrescriptionStatus"}) +const logger = new Logger({serviceName: "GSUL"}) const client = new DynamoDBClient({region: "eu-west-2"}) const docClient = DynamoDBDocumentClient.from(client) const tableName = process.env.TABLE_NAME const lambdaHandler = async (event: requestType): Promise => { + // there are deliberately no try..catch blocks in this as any errors are caught by custom middy error handler + // and an error response is sent const queryParams = event.prescriptions.map((prescription) => { // create query for each prescription and ods code passed in @@ -34,7 +37,7 @@ const lambdaHandler = async (event: requestType): Promise => { }) // run the dynamodb queries - const queryResultsTasks = await runDynamoDBQueries(queryParams) + const queryResultsTasks = runDynamoDBQueries(queryParams, docClient, logger) // get all the query results const queryResults = await Promise.all(queryResultsTasks) @@ -49,40 +52,6 @@ const lambdaHandler = async (event: requestType): Promise => { return response } -const runDynamoDBQueries = (queryParams: Array): Array>> => { - const queryResultsTasks: Array>> = queryParams.map(async (query) => { - // run each query - const command = new QueryCommand(query) - logger.info("running query", {query}) - const dynamoDBresponse = await docClient.send(command) - if (dynamoDBresponse?.Count !== 0) { - - const response: Array = dynamoDBresponse.Items?.map((singleUpdate) => { - const result: DynamoDBResult = { - prescriptionID: String(singleUpdate.PrescriptionID), - itemId: String(singleUpdate.LineItemID), - latestStatus: String(singleUpdate.Status), - isTerminalState: String(singleUpdate.TerminalStatus), - lastUpdateDateTime: String(singleUpdate.LastModified) - } - return result - }) - - return response - } - const result: Array = [{ - prescriptionID: undefined, - itemId: undefined, - latestStatus: undefined, - isTerminalState: undefined, - lastUpdateDateTime: undefined - }] - return result - }) - - return queryResultsTasks -} - export const buildResults = (inputPrescriptions: Array, queryResults: Array): Array => { // for each prescription id passed in build up a response diff --git a/packages/gsul/src/schema/request.ts b/packages/gsul/src/schema/request.ts index ba3057023..e329df69c 100644 --- a/packages/gsul/src/schema/request.ts +++ b/packages/gsul/src/schema/request.ts @@ -18,7 +18,9 @@ const requestSchema = { required: ["schemaVersion", "prescriptions"], properties: { schemaVersion: { - type: "number" + type: "number", + minimum: 1, + maximum: 1 }, prescriptions: { type: "array", diff --git a/packages/gsul/tests/testErrorHandler.test.ts b/packages/gsul/tests/testErrorHandler.test.ts new file mode 100644 index 000000000..064172e97 --- /dev/null +++ b/packages/gsul/tests/testErrorHandler.test.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {errorHandler} from "../src/errorHandler" +import middy from "@middy/core" +import {expect, jest} from "@jest/globals" + +const mockEvent = { + foo: "bar" +} + +const dummyContext = { + callbackWaitsForEmptyEventLoop: true, + functionVersion: "$LATEST", + functionName: "foo-bar-function", + memoryLimitInMB: "128", + logGroupName: "/aws/lambda/foo-bar-function-123456abcdef", + logStreamName: "2021/03/09/[$LATEST]abcdef123456abcdef123456abcdef123456", + invokedFunctionArn: "arn:aws:lambda:eu-west-1:123456789012:function:foo-bar-function", + awsRequestId: "c6af9ac6-7b61-11e6-9a41-93e812345678", + getRemainingTimeInMillis: () => 1234, + done: () => console.log("Done!"), + fail: () => console.log("Failed!"), + succeed: () => console.log("Succeeded!") +} + +test("Middleware logs all error details", async () => { + type ErrorLogger = (error: any, message: string) => void + const mockErrorLogger: jest.MockedFunction = jest.fn() + const mockLogger = { + error: mockErrorLogger + } + + const handler = middy(() => { + throw new Error("error running lambda") + }) + + handler.use(errorHandler({logger: mockLogger})) + + await handler({}, dummyContext) + + expect(mockErrorLogger).toHaveBeenCalledTimes(1) + + const [errorObject, errorMessage] = mockErrorLogger.mock.calls[mockErrorLogger.mock.calls.length - 1] + expect(errorMessage).toBe("Error: error running lambda") + expect(errorObject.error.name).toBe("Error") + expect(errorObject.error.message).toBe("error running lambda") + expect(errorObject.error.stack).not.toBeNull() +}) + +test("Middleware returns details as valid fhir from lambda event", async () => { + const mockLogger = { + error: jest.fn(() => {}) + } + + const handler = middy(() => { + throw new Error("error running lambda") + }) + + handler.use(errorHandler({logger: mockLogger})) + + const response = await handler(mockEvent, dummyContext) + expect(response).toMatchObject({ + schemaVersion: 1, + isSuccess: false, + prescriptions: [] + }) + +}) diff --git a/packages/gsul/tests/testGetStatusUpdates.test.ts b/packages/gsul/tests/testGetStatusUpdates.test.ts index f727f668a..58ed9a053 100644 --- a/packages/gsul/tests/testGetStatusUpdates.test.ts +++ b/packages/gsul/tests/testGetStatusUpdates.test.ts @@ -104,21 +104,33 @@ const scenarios: Array = [ latestStatus: "item_1_status", isTerminalState: "is_terminal_status", lastUpdateDateTime: "1970-01-01T00:00:00Z" + }, { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_item_1_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1972-01-01T00:00:00Z" }, { prescriptionID: "abc", itemId: "item_2", latestStatus: "item_2_status", isTerminalState: "is_terminal_status", lastUpdateDateTime: "1971-01-01T00:00:00Z" + }, { + prescriptionID: "abc", + itemId: "item_2", + latestStatus: "early_item_2_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" }], expectedResult: [{ prescriptionID: "abc", onboarded: true, items: [{ itemId: "item_1", - latestStatus: "item_1_status", + latestStatus: "latest_item_1_status", isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" + lastUpdateDateTime: "1972-01-01T00:00:00Z" }, { itemId: "item_2", latestStatus: "item_2_status", diff --git a/packages/gsul/tests/testHander.test.ts b/packages/gsul/tests/testHander.test.ts new file mode 100644 index 000000000..a9596dd10 --- /dev/null +++ b/packages/gsul/tests/testHander.test.ts @@ -0,0 +1,69 @@ +import { + expect, + describe, + it, + jest +} from "@jest/globals" +import {handler} from "../src/getStatusUpdates" +import {runDynamoDBQueries} from "../src/dynamoDBclient.ts" +import {DynamoDBResult} from "../src/schema/result" + +const dummyContext = { + callbackWaitsForEmptyEventLoop: true, + functionVersion: "$LATEST", + functionName: "foo-bar-function", + memoryLimitInMB: "128", + logGroupName: "/aws/lambda/foo-bar-function-123456abcdef", + logStreamName: "2021/03/09/[$LATEST]abcdef123456abcdef123456abcdef123456", + invokedFunctionArn: "arn:aws:lambda:eu-west-1:123456789012:function:foo-bar-function", + awsRequestId: "c6af9ac6-7b61-11e6-9a41-93e812345678", + getRemainingTimeInMillis: () => 1234, + done: () => console.log("Done!"), + fail: () => console.log("Failed!"), + succeed: () => console.log("Succeeded!") +} + +function mockRunDynamoDBQueries(): Array>> { + const emptyResult: Array = [{ + prescriptionID: undefined, + itemId: undefined, + latestStatus: undefined, + isTerminalState: undefined, + lastUpdateDateTime: undefined + }] + const emptyPromise: Promise> = Promise.resolve(emptyResult) + const emptyFinal: Array>> = [emptyPromise, emptyPromise] + return emptyFinal +} + +describe("test handler", () => { + beforeEach(() => { + jest.resetModules() + jest.clearAllMocks() + jest.mock("../src/dynamoDBclient.ts", () => ({ + runDynamoDBQueries: jest.fn().mockImplementation(mockRunDynamoDBQueries), + })) + }) + + it("respond with error when schema version is 2", async () => { + const response = await handler({schemaVersion: 2}, dummyContext) + expect(response).toMatchObject({ + schemaVersion: 1, + isSuccess: false, + prescriptions: [] + }) + }) + + it("respond with success when everything is correct", async () => { + const response = await handler( + { + "schemaVersion": 1, + "prescriptions": [] + }, dummyContext) + expect(response).toMatchObject({ + schemaVersion: 1, + isSuccess: true, + prescriptions: [] + }) + }) +}) diff --git a/packages/gsul/tests/testRunDynamoDBQueries.test.ts b/packages/gsul/tests/testRunDynamoDBQueries.test.ts new file mode 100644 index 000000000..ac14a0bf8 --- /dev/null +++ b/packages/gsul/tests/testRunDynamoDBQueries.test.ts @@ -0,0 +1,158 @@ +import {QueryCommandInput, DynamoDBDocumentClient} from "@aws-sdk/lib-dynamodb" +import {DynamoDBClient} from "@aws-sdk/client-dynamodb" +import {runDynamoDBQueries} from "../src/dynamoDBclient" +import {Logger} from "@aws-lambda-powertools/logger" +import { + expect, + describe, + it, + jest +} from "@jest/globals" + +const logger = new Logger({serviceName: "GSUL_TEST"}) + +const client = new DynamoDBClient({region: "eu-west-2"}) +const docClient = DynamoDBDocumentClient.from(client) + +describe("testing dynamoDBClient", () => { + beforeEach(() => { + jest.resetModules() + jest.clearAllMocks() + const mockReply = { + Count: 1, + Items: [{ + PrescriptionID: "abc", + LineItemID: "item_1", + Status: "latest_status", + TerminalStatus: "is_terminal_status", + LastModified: "1970-01-01T00:00:00Z" + }] + } + jest.spyOn(DynamoDBDocumentClient.prototype, "send").mockResolvedValue(mockReply as never) + }) + + it("should call dynamo once and return expected items", async () => { + const queryParams : Array = [{ + TableName: "tableName", + IndexName: "indexName", + KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", + ExpressionAttributeValues: { + ":inputPharmacyODSCode": "odsCode", + ":inputPrescriptionID": "prescriptionID" + } + }] + const queryResultsTasks = runDynamoDBQueries(queryParams, docClient, logger) + const queryResults = await Promise.all(queryResultsTasks) + const itemResults = queryResults.flat() + expect(DynamoDBDocumentClient.prototype.send).toHaveBeenCalledTimes(1) + expect(itemResults).toMatchObject([{ + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }]) + }) + + it("should call dynamo twice and return expected items", async () => { + const queryParams : Array = [{ + TableName: "tableName", + IndexName: "indexName", + KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", + ExpressionAttributeValues: { + ":inputPharmacyODSCode": "odsCode", + ":inputPrescriptionID": "prescriptionID" + } + }, { + TableName: "tableName", + IndexName: "indexName", + KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", + ExpressionAttributeValues: { + ":inputPharmacyODSCode": "odsCode", + ":inputPrescriptionID": "prescriptionID" + } + }] + const queryResultsTasks = runDynamoDBQueries(queryParams, docClient, logger) + const queryResults = await Promise.all(queryResultsTasks) + const itemResults = queryResults.flat() + expect(DynamoDBDocumentClient.prototype.send).toHaveBeenCalledTimes(2) + expect(itemResults).toMatchObject([{ + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }, { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }]) + }) +}) + +describe("testing pagination in dynamoDBClient", () => { + beforeEach(() => { + jest.resetModules() + jest.clearAllMocks() + const mockFirstReply = { + Count: 1, + Items: [{ + PrescriptionID: "abc", + LineItemID: "item_1", + Status: "first_status", + TerminalStatus: "is_terminal_status", + LastModified: "1970-01-01T00:00:00Z" + }], + LastEvaluatedKey: { + PharmacyODSCode: "C9Z1O", + RequestID: "d90b88b-9cc8-4b70-9d9f-0144adcc38cc", + PrescriptionID: "16B2E0-A83008-81C13H" + } + } + const mockSecondReply = { + Count: 1, + Items: [{ + PrescriptionID: "abc", + LineItemID: "item_1", + Status: "second_status", + TerminalStatus: "is_terminal_status", + LastModified: "1970-01-01T00:00:00Z" + }] + } + jest.spyOn(DynamoDBDocumentClient.prototype, "send") + .mockResolvedValueOnce(mockFirstReply as never) + .mockResolvedValueOnce(mockSecondReply as never) + }) + + it("should handle pagination", async () => { + const queryParams : Array = [{ + TableName: "tableName", + IndexName: "indexName", + KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", + ExpressionAttributeValues: { + ":inputPharmacyODSCode": "odsCode", + ":inputPrescriptionID": "prescriptionID" + } + }] + const queryResultsTasks = runDynamoDBQueries(queryParams, docClient, logger) + const queryResults = await Promise.all(queryResultsTasks) + const itemResults = queryResults.flat() + + expect(itemResults).toMatchObject([{ + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "first_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }, { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "second_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }]) + expect(DynamoDBDocumentClient.prototype.send).toHaveBeenCalledTimes(2) + }) +}) From 3178e955fa88dc4b8920b295add844576634ae7c Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:20:07 +0000 Subject: [PATCH 05/33] reset changes in prepartion for merge conflict --- SAMtemplates/main_template.yaml | 73 ++++++++----------- packages/gsul/src/dynamoDBclient.ts | 2 +- packages/gsul/tests/testHander.test.ts | 3 +- .../src/updatePrescriptionStatus.ts | 6 +- .../tests/test-handler.test.ts | 46 +++--------- 5 files changed, 45 insertions(+), 85 deletions(-) diff --git a/SAMtemplates/main_template.yaml b/SAMtemplates/main_template.yaml index 32f2d53c6..99bcbc089 100644 --- a/SAMtemplates/main_template.yaml +++ b/SAMtemplates/main_template.yaml @@ -115,40 +115,6 @@ Resources: tsconfig: updatePrescriptionStatus/tsconfig.json EntryPoints: - updatePrescriptionStatus/src/updatePrescriptionStatus.ts - - # updatePrescriptionStatus lambda - GSULResources: - Type: AWS::Serverless::Application - Properties: - Location: lambda_resources.yaml - Parameters: - CloudWatchKMSKey: !ImportValue account-resources:CloudwatchLogsKmsKeyArn - SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole - SplunkDeliveryStream: !ImportValue lambda-resources:SplunkDeliveryStream - EnableSplunk: !Ref EnableSplunk - LambdaName: !Sub "${AWS::StackName}-GSUL" - LogRetentionDays: !Ref LogRetentionDays - GSUL: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Sub "${AWS::StackName}-GSUL" - CodeUri: ../packages - Handler: "getStatusUpdates.handler" - Role: !GetAtt GSULResources.Outputs.LambdaRoleArn - Environment: - Variables: - TABLE_NAME: !Sub "${AWS::StackName}-PrescriptionStatusUpdates" - LOG_LEVEL: !Ref LogLevel - Metadata: # Manage esbuild properties - BuildMethod: esbuild - BuildProperties: - Minify: true - Target: "es2020" - Sourcemap: true - tsconfig: gsul/tsconfig.json - EntryPoints: - - gsul/src/getStatusUpdates.ts - # Prescription Status Updates table PrescriptionStatusUpdatesTable: Type: "AWS::DynamoDB::Table" @@ -172,19 +138,38 @@ Resources: ReadCapacityUnits: "5" WriteCapacityUnits: "5" GlobalSecondaryIndexes: + - IndexName: "RequestIDIndex" + KeySchema: + - AttributeName: "RequestID" + KeyType: "HASH" + - AttributeName: "PrescriptionID" + KeyType: "RANGE" + Projection: + NonKeyAttributes: + - "PatientNHSNumber" + - "PharmacyODSCode" + - "TaskID" + - "LineItemID" + - "TerminalStatus" + - "RequestMessage" + ProjectionType: "INCLUDE" + ProvisionedThroughput: + ReadCapacityUnits: "5" + WriteCapacityUnits: "5" - IndexName: "PrescriptionIDIndex" KeySchema: - AttributeName: "PrescriptionID" KeyType: "HASH" - - AttributeName: "PharmacyODSCode" + - AttributeName: "PatientNHSNumber" KeyType: "RANGE" Projection: NonKeyAttributes: + - "PharmacyODSCode" + - "TaskID" - "LineItemID" - "TerminalStatus" - - "LastModified" - - "Status" - - "PatientNHSNumber" + - "RequestID" + - "RequestMessage" ProjectionType: "INCLUDE" ProvisionedThroughput: ReadCapacityUnits: "5" @@ -198,10 +183,11 @@ Resources: Projection: NonKeyAttributes: - "PharmacyODSCode" + - "TaskID" - "LineItemID" - "TerminalStatus" - - "LastModified" - - "Status" + - "RequestID" + - "RequestMessage" ProjectionType: "INCLUDE" ProvisionedThroughput: ReadCapacityUnits: "5" @@ -215,10 +201,11 @@ Resources: Projection: NonKeyAttributes: - "PatientNHSNumber" + - "TaskID" - "LineItemID" - "TerminalStatus" - - "LastModified" - - "Status" + - "RequestID" + - "RequestMessage" ProjectionType: "INCLUDE" ProvisionedThroughput: ReadCapacityUnits: "5" @@ -227,7 +214,7 @@ Resources: Type: AWS::IAM::ManagedPolicy Properties: Roles: - - !GetAtt GSULResources.Outputs.LambdaRoleName + - !GetAtt UpdatePrescriptionStatusResources.Outputs.LambdaRoleName PolicyDocument: Version: "2012-10-17" Statement: diff --git a/packages/gsul/src/dynamoDBclient.ts b/packages/gsul/src/dynamoDBclient.ts index 7c0677057..c27871d44 100644 --- a/packages/gsul/src/dynamoDBclient.ts +++ b/packages/gsul/src/dynamoDBclient.ts @@ -16,7 +16,7 @@ export function runDynamoDBQueries (queryParams: Array, } const command = new QueryCommand(query) logger.info("running query", {query}) - return docClient.send(new QueryCommand(query)) + return docClient.send(command) } let lastEvaluatedKey = null let rows = [] diff --git a/packages/gsul/tests/testHander.test.ts b/packages/gsul/tests/testHander.test.ts index a9596dd10..79a77d764 100644 --- a/packages/gsul/tests/testHander.test.ts +++ b/packages/gsul/tests/testHander.test.ts @@ -5,7 +5,6 @@ import { jest } from "@jest/globals" import {handler} from "../src/getStatusUpdates" -import {runDynamoDBQueries} from "../src/dynamoDBclient.ts" import {DynamoDBResult} from "../src/schema/result" const dummyContext = { @@ -41,7 +40,7 @@ describe("test handler", () => { jest.resetModules() jest.clearAllMocks() jest.mock("../src/dynamoDBclient.ts", () => ({ - runDynamoDBQueries: jest.fn().mockImplementation(mockRunDynamoDBQueries), + runDynamoDBQueries: jest.fn().mockImplementation(mockRunDynamoDBQueries) })) }) diff --git a/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts b/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts index 0fd934584..bca1aa08b 100644 --- a/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts +++ b/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts @@ -21,8 +21,6 @@ interface DynamoDBItem { LineItemID: string; TerminalStatus: string; RequestMessage: any; - LastModified: string; - Status: string } const lambdaHandler = async (event: APIGatewayProxyEvent): Promise => { @@ -79,9 +77,7 @@ const lambdaHandler = async (event: APIGatewayProxyEvent): Promise { PharmacyODSCode: string, PrescriptionID: string, TaskID: string, - TerminalStatus: string, - LastModified: string, - Status: string + TerminalStatus: string ) => ({ LineItemID: {S: LineItemID}, PatientNHSNumber: {S: PatientNHSNumber}, @@ -66,13 +64,9 @@ describe("Unit test for updatePrescriptionStatus handler", () => { for: {M: {identifier: {M: {value: {S: PatientNHSNumber}}}}}, id: {S: TaskID}, owner: {M: {identifier: {M: {value: {S: PharmacyODSCode}}}}}, - status: {S: TerminalStatus}, - lastModified: {S: LastModified}, - businessStatus: {M: {coding: {L: [{M: {display: Status}}]}}} + status: {S: TerminalStatus} } - }, - LastModified: {S: LastModified}, - Status: {S: Status} + } }) beforeEach(() => { @@ -92,20 +86,18 @@ describe("Unit test for updatePrescriptionStatus handler", () => { owner: {identifier: {value: "PharmacyODSCode"}}, id: "TaskID", focus: {identifier: {value: "LineItemID"}}, - status: "TerminalStatus", - lastModified: "1970-01-01T00:00:00Z", - businessStatus: {coding: [{display: "dispatched"}]} + status: "TerminalStatus" } } ] }) } - const spy = jest.spyOn(DynamoDBClient.prototype, "send").mockResolvedValue(undefined as never) + jest.spyOn(DynamoDBClient.prototype, "send").mockResolvedValue(undefined as never) const result: APIGatewayProxyResult = await handler(event, dummyContext) - expect(spy).toHaveBeenCalledWith( + expect(DynamoDBClient.prototype.send).toHaveBeenCalledWith( expect.objectContaining({ input: expect.objectContaining({ Item: expect.objectContaining( @@ -115,9 +107,7 @@ describe("Unit test for updatePrescriptionStatus handler", () => { "PharmacyODSCode", "PrescriptionID", "TaskID", - "TerminalStatus", - "1970-01-01T00:00:00Z", - "dispatched" + "TerminalStatus" ) ) }) @@ -140,10 +130,7 @@ describe("Unit test for updatePrescriptionStatus handler", () => { owner: {identifier: {value: "PharmacyODSCode1"}}, id: "TaskID1", focus: {identifier: {value: "LineItemID1"}}, - status: "TerminalStatus1", - lastModified: "1970-01-01T00:00:00Z", - businessStatus: {coding: [{display: "dispatched"}]} - + status: "TerminalStatus1" } }, { @@ -153,10 +140,7 @@ describe("Unit test for updatePrescriptionStatus handler", () => { owner: {identifier: {value: "PharmacyODSCode2"}}, id: "TaskID2", focus: {identifier: {value: "LineItemID2"}}, - status: "TerminalStatus2", - lastModified: "1970-01-01T00:00:00Z", - businessStatus: {coding: [{display: "dispatched"}]} - + status: "TerminalStatus2" } } ] @@ -185,9 +169,7 @@ describe("Unit test for updatePrescriptionStatus handler", () => { "PharmacyODSCode1", "PrescriptionID1", "TaskID1", - "TerminalStatus1", - "1970-01-01T00:00:00Z", - "dispatched" + "TerminalStatus1" ) ) }) @@ -204,9 +186,7 @@ describe("Unit test for updatePrescriptionStatus handler", () => { "PharmacyODSCode2", "PrescriptionID2", "TaskID2", - "TerminalStatus2", - "1970-01-01T00:00:00Z", - "dispatched" + "TerminalStatus2" ) ) }) @@ -304,9 +284,7 @@ describe("Unit test for updatePrescriptionStatus handler", () => { owner: {identifier: {value: "PharmacyODSCode"}}, id: "TaskID", focus: {identifier: {value: "LineItemID"}}, - status: "TerminalStatus", - lastModified: "1970-01-01T00:00:00Z", - businessStatus: {coding: [{display: "dispatched"}]} + status: "TerminalStatus" } } ] From 11c1aba914493be492b7fd85680fcb4f8df5adfa Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:24:33 +0000 Subject: [PATCH 06/33] add new package to sonar --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 6f3d365f6..1ebf9fc29 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,4 +3,4 @@ sonar.projectKey=NHSDigital_eps-prescription-status-update-api sonar.host.url=https://sonarcloud.io sonar.coverage.exclusions=**/*.test.*,**/jest.config.ts,scripts/*,packages/specification/* -sonar.javascript.lcov.reportPaths=packages/updatePrescriptionStatus/coverage/lcov.info +sonar.javascript.lcov.reportPaths=packages/updatePrescriptionStatus/coverage/lcov.info,packages/gsul/coverage/lcov.info From 942555e2d3c13ac241487c022627a20d929cb58c Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:29:22 +0000 Subject: [PATCH 07/33] run gsul tests --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 68d17f247..836af4a97 100644 --- a/Makefile +++ b/Makefile @@ -108,6 +108,7 @@ lint: lint-node lint-samtemplates lint-python lint-githubactions lint-githubacti test: compile npm run test --workspace packages/updatePrescriptionStatus + npm run test --workspace packages/gsul #Removes build/ + dist/ directories clean: From 0d44d9790da61e5f6df70fbaec9a52865cf97209 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:44:31 +0000 Subject: [PATCH 08/33] sonar stuff --- packages/gsul/src/errorHandler.ts | 2 +- packages/gsul/src/getStatusUpdates.ts | 2 +- .../{testGetStatusUpdates.test.ts => testBuildResults.test.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/gsul/tests/{testGetStatusUpdates.test.ts => testBuildResults.test.ts} (100%) diff --git a/packages/gsul/src/errorHandler.ts b/packages/gsul/src/errorHandler.ts index edf0896b2..4abbe685a 100644 --- a/packages/gsul/src/errorHandler.ts +++ b/packages/gsul/src/errorHandler.ts @@ -17,7 +17,7 @@ type LoggerAndLevel = { function errorHandler({logger = console, level = "error"}: LoggerAndLevel) { return { onError: async (handler) => { - const error: Error | any = handler.error + const error: any = handler.error // if there are a `statusCode` and an `error` field // this is a valid http error object diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index 8384d4471..c496e4acd 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -118,7 +118,7 @@ export const handler = middy(lambdaHandler) inputOutputLogger({ logger: (request) => { if (request.response) { - logger.info(request) + logger.info(request.response) } else { logger.info(request) } diff --git a/packages/gsul/tests/testGetStatusUpdates.test.ts b/packages/gsul/tests/testBuildResults.test.ts similarity index 100% rename from packages/gsul/tests/testGetStatusUpdates.test.ts rename to packages/gsul/tests/testBuildResults.test.ts From f801666925d323484fe4e641292a047b451578b8 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:28:21 +0000 Subject: [PATCH 09/33] tests for handler --- packages/gsul/tests/testHander.test.ts | 65 ++++++++++++++++++-------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/packages/gsul/tests/testHander.test.ts b/packages/gsul/tests/testHander.test.ts index 79a77d764..c9828ed3f 100644 --- a/packages/gsul/tests/testHander.test.ts +++ b/packages/gsul/tests/testHander.test.ts @@ -4,8 +4,9 @@ import { it, jest } from "@jest/globals" + +import {DynamoDBDocumentClient} from "@aws-sdk/lib-dynamodb" import {handler} from "../src/getStatusUpdates" -import {DynamoDBResult} from "../src/schema/result" const dummyContext = { callbackWaitsForEmptyEventLoop: true, @@ -22,28 +23,11 @@ const dummyContext = { succeed: () => console.log("Succeeded!") } -function mockRunDynamoDBQueries(): Array>> { - const emptyResult: Array = [{ - prescriptionID: undefined, - itemId: undefined, - latestStatus: undefined, - isTerminalState: undefined, - lastUpdateDateTime: undefined - }] - const emptyPromise: Promise> = Promise.resolve(emptyResult) - const emptyFinal: Array>> = [emptyPromise, emptyPromise] - return emptyFinal -} - describe("test handler", () => { beforeEach(() => { jest.resetModules() jest.clearAllMocks() - jest.mock("../src/dynamoDBclient.ts", () => ({ - runDynamoDBQueries: jest.fn().mockImplementation(mockRunDynamoDBQueries) - })) }) - it("respond with error when schema version is 2", async () => { const response = await handler({schemaVersion: 2}, dummyContext) expect(response).toMatchObject({ @@ -53,7 +37,13 @@ describe("test handler", () => { }) }) - it("respond with success when everything is correct", async () => { + it("respond with success for empty request", async () => { + const mockReply = { + Count: 0, + Items: [] + } + jest.spyOn(DynamoDBDocumentClient.prototype, "send").mockResolvedValue(mockReply as never) + const response = await handler( { "schemaVersion": 1, @@ -65,4 +55,41 @@ describe("test handler", () => { prescriptions: [] }) }) + + it("respond with success when data passed in", async () => { + const mockReply = { + Count: 1, + Items: [{ + PrescriptionID: "abc", + LineItemID: "item_1", + Status: "latest_status", + TerminalStatus: "terminal", + LastModified: "1970-01-01T00:00:00Z" + }] + } + jest.spyOn(DynamoDBDocumentClient.prototype, "send").mockResolvedValue(mockReply as never) + + const response = await handler( + { + "schemaVersion": 1, + "prescriptions": [{ + prescriptionID: "abc", + odsCode: "123" + }] + }, dummyContext) + expect(response).toMatchObject({ + schemaVersion: 1, + isSuccess: true, + prescriptions: [{ + prescriptionID: "abc", + onboarded: true, + items: [{ + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "terminal", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }] + }] + }) + }) }) From a853093f922dd8a143819c3b25f9ca6c88b1770a Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:42:33 +0000 Subject: [PATCH 10/33] add some comments --- packages/gsul/src/getStatusUpdates.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index c496e4acd..e0d6a5ef4 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -42,6 +42,10 @@ const lambdaHandler = async (event: requestType): Promise => { // get all the query results const queryResults = await Promise.all(queryResultsTasks) + // queryResults is an array of arrays + // the first array is for each prescription id passed in as we have created a query for each prescription + // and the second array is the items for each prescription id + // we use flat() to just get an array of items which we can pass to buildResults const itemResults = buildResults(event.prescriptions, queryResults.flat()) const response = { From 51732c4851a64bf206298280ad335c211c29cd51 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:44:01 +0000 Subject: [PATCH 11/33] wording --- packages/gsul/src/getStatusUpdates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index e0d6a5ef4..3ca8440bd 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -44,7 +44,7 @@ const lambdaHandler = async (event: requestType): Promise => { // queryResults is an array of arrays // the first array is for each prescription id passed in as we have created a query for each prescription - // and the second array is the items for each prescription id + // and the second array is the items for that prescription id // we use flat() to just get an array of items which we can pass to buildResults const itemResults = buildResults(event.prescriptions, queryResults.flat()) From 0cab29e24fe1a37ccc82871549f3979e558de395 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:47:36 +0000 Subject: [PATCH 12/33] better test --- packages/gsul/tests/testHander.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/gsul/tests/testHander.test.ts b/packages/gsul/tests/testHander.test.ts index c9828ed3f..5def4371a 100644 --- a/packages/gsul/tests/testHander.test.ts +++ b/packages/gsul/tests/testHander.test.ts @@ -28,8 +28,15 @@ describe("test handler", () => { jest.resetModules() jest.clearAllMocks() }) + it("respond with error when schema version is 2", async () => { - const response = await handler({schemaVersion: 2}, dummyContext) + const response = await handler({ + "schemaVersion": 2, + "prescriptions": [{ + prescriptionID: "abc", + odsCode: "123" + }] + }, dummyContext) expect(response).toMatchObject({ schemaVersion: 1, isSuccess: false, From 270de5a9c0560e3da0251548f5e38d551c559fb6 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:16:28 +0000 Subject: [PATCH 13/33] deploy lambda --- SAMtemplates/functions/main.yaml | 57 +++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/SAMtemplates/functions/main.yaml b/SAMtemplates/functions/main.yaml index 1e9df35d0..dd322f8a6 100644 --- a/SAMtemplates/functions/main.yaml +++ b/SAMtemplates/functions/main.yaml @@ -1,4 +1,4 @@ -AWSTemplateFormatVersion: '2010-09-09' +AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: | PSU lambda functions and related resources @@ -12,7 +12,7 @@ Globals: Runtime: nodejs20.x Environment: Variables: - NODE_OPTIONS: '--enable-source-maps' + NODE_OPTIONS: "--enable-source-maps" Layers: - !Sub arn:aws:lambda:${AWS::Region}:580247275435:layer:LambdaInsightsExtension:49 @@ -20,17 +20,17 @@ Parameters: StackName: Type: String Default: none - + PrescriptionStatusUpdatesTableName: Type: String Default: none - + LogLevel: Type: String - + LogRetentionInDays: Type: Number - + EnableSplunk: Type: String @@ -55,7 +55,7 @@ Resources: tsconfig: updatePrescriptionStatus/tsconfig.json EntryPoints: - updatePrescriptionStatus/src/updatePrescriptionStatus.ts - + UpdatePrescriptionStatusResources: Type: AWS::Serverless::Application Properties: @@ -66,7 +66,7 @@ Resources: LambdaArn: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${StackName}-UpdatePrescriptionStatus IncludeAdditionalPolicies: true AdditionalPolicies: !Join - - ',' + - "," - - Fn::ImportValue: !Sub ${StackName}:tables:${PrescriptionStatusUpdatesTableName}:TableWritePolicyArn LogRetentionInDays: !Ref LogRetentionInDays CloudWatchKMSKeyId: !ImportValue account-resources:CloudwatchLogsKmsKeyArn @@ -74,6 +74,45 @@ Resources: SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole SplunkDeliveryStreamArn: !ImportValue lambda-resources:SplunkDeliveryStream + GUSL: + Type: AWS::Serverless::Function + Properties: + FunctionName: !Sub ${StackName}-GetStatusUpdates + CodeUri: ../../packages + Handler: getStatusUpdates.handler + Role: !GetAtt GSULResources.Outputs.LambdaRoleArn + Environment: + Variables: + TABLE_NAME: !Ref PrescriptionStatusUpdatesTableName + LOG_LEVEL: !Ref LogLevel + Metadata: + BuildMethod: esbuild + BuildProperties: + Minify: true + Target: es2020 + Sourcemap: true + tsconfig: gsul/tsconfig.json + EntryPoints: + - gsul/src/getStatusUpdates.ts + + GSULResources: + Type: AWS::Serverless::Application + Properties: + Location: lambda_resources.yaml + Parameters: + StackName: !Ref StackName + LambdaName: !Sub ${StackName}-GetStatusUpdates + LambdaArn: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${StackName}-GetStatusUpdates + IncludeAdditionalPolicies: true + AdditionalPolicies: !Join + - "," + - - Fn::ImportValue: !Sub ${StackName}:tables:${PrescriptionStatusUpdatesTableName}:TableReadPolicyArn + LogRetentionInDays: !Ref LogRetentionInDays + CloudWatchKMSKeyId: !ImportValue account-resources:CloudwatchLogsKmsKeyArn + EnableSplunk: !Ref EnableSplunk + SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole + SplunkDeliveryStreamArn: !ImportValue lambda-resources:SplunkDeliveryStream + Outputs: UpdatePrescriptionStatusFunctionName: Description: The function name of the UpdatePrescriptionStatus lambda @@ -81,4 +120,4 @@ Outputs: UpdatePrescriptionStatusFunctionArn: Description: The function ARN of the UpdatePrescriptionStatus lambda - Value: !GetAtt UpdatePrescriptionStatus.Arn + Value: !GetAtt UpdatePrescriptionStatus.Arn From 5879fef91cf8c384ae6ee77c704353c6ee4c2113 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Tue, 9 Apr 2024 06:30:57 +0000 Subject: [PATCH 14/33] use pull request splunk --- SAMtemplates/state_machines/main.yaml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/SAMtemplates/state_machines/main.yaml b/SAMtemplates/state_machines/main.yaml index b48b9dae4..51c7826a8 100644 --- a/SAMtemplates/state_machines/main.yaml +++ b/SAMtemplates/state_machines/main.yaml @@ -1,4 +1,4 @@ -AWSTemplateFormatVersion: '2010-09-09' +AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: | PSU state machines and related resources @@ -7,18 +7,18 @@ Parameters: StackName: Type: String Default: none - + UpdatePrescriptionStatusFunctionName: Type: String Default: none - + UpdatePrescriptionStatusFunctionArn: Type: String Default: none - + LogRetentionInDays: Type: Number - + EnableSplunk: Type: String @@ -32,7 +32,7 @@ Resources: DefinitionUri: UpdatePrescriptionStatusStateMachine.asl.json DefinitionSubstitutions: FhirValidationFunctionArn: !Join - - ":" + - ":" - - !ImportValue fhir-validator:FHIRValidatorUKCoreLambdaArn - snap UpdatePrescriptionStatusFunctionArn: !Sub ${UpdatePrescriptionStatusFunctionArn}:$LATEST @@ -44,7 +44,7 @@ Resources: Level: ALL Tracing: Enabled: true - + UpdatePrescriptionStatusStateMachineResources: Type: AWS::Serverless::Application Properties: @@ -54,20 +54,20 @@ Resources: StateMachineName: !Sub ${StackName}-UpdatePrescriptionStatus StateMachineArn: !Sub arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${StackName}-UpdatePrescriptionStatus AdditionalPolicies: !Join - - ',' + - "," - - Fn::ImportValue: !Sub ${StackName}:functions:${UpdatePrescriptionStatusFunctionName}:ExecuteLambdaPolicyArn - !ImportValue fhir-validator:FHIRValidatorUKCoreExecuteLambdaPolicyArn LogRetentionInDays: !Ref LogRetentionInDays CloudWatchKMSKeyId: !ImportValue account-resources:CloudwatchLogsKmsKeyArn EnableSplunk: !Ref EnableSplunk - SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole - SplunkDeliveryStreamArn: !ImportValue lambda-resources:SplunkDeliveryStream - + SplunkSubscriptionFilterRole: !ImportValue lambda-resources-pr-133:SplunkSubscriptionFilterRole + SplunkDeliveryStreamArn: !ImportValue lambda-resources-pr-133:SplunkDeliveryStream + Outputs: UpdatePrescriptionStatusStateMachineArn: Description: UpdatePrescriptionStatus state machine arn Value: !Ref UpdatePrescriptionStatusStateMachine - + UpdatePrescriptionStatusStateMachineName: Description: UpdatePrescriptionStatus state machine name - Value: !GetAtt UpdatePrescriptionStatusStateMachine.Name + Value: !GetAtt UpdatePrescriptionStatusStateMachine.Name From 34aa6e34fe47bc3a56f675fe5e1008ca893ebeb1 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Tue, 9 Apr 2024 07:32:58 +0000 Subject: [PATCH 15/33] use normal splunk --- SAMtemplates/state_machines/main.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SAMtemplates/state_machines/main.yaml b/SAMtemplates/state_machines/main.yaml index 51c7826a8..fb9e6a3fc 100644 --- a/SAMtemplates/state_machines/main.yaml +++ b/SAMtemplates/state_machines/main.yaml @@ -60,8 +60,8 @@ Resources: LogRetentionInDays: !Ref LogRetentionInDays CloudWatchKMSKeyId: !ImportValue account-resources:CloudwatchLogsKmsKeyArn EnableSplunk: !Ref EnableSplunk - SplunkSubscriptionFilterRole: !ImportValue lambda-resources-pr-133:SplunkSubscriptionFilterRole - SplunkDeliveryStreamArn: !ImportValue lambda-resources-pr-133:SplunkDeliveryStream + SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole + SplunkDeliveryStreamArn: !ImportValue lambda-resources:SplunkDeliveryStream Outputs: UpdatePrescriptionStatusStateMachineArn: From f94bb3ba70e69929eb89eadf4f66183d07c193cf Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:05:50 +0000 Subject: [PATCH 16/33] use a new log group name for state machine --- .../state_machine_resources.yaml | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/SAMtemplates/state_machines/state_machine_resources.yaml b/SAMtemplates/state_machines/state_machine_resources.yaml index 11da486b8..d590ca1b7 100644 --- a/SAMtemplates/state_machines/state_machine_resources.yaml +++ b/SAMtemplates/state_machines/state_machine_resources.yaml @@ -1,4 +1,4 @@ -AWSTemplateFormatVersion: '2010-09-09' +AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: | Resources for a state machine @@ -7,11 +7,11 @@ Parameters: StackName: Type: String Default: none - + StateMachineName: Type: String Default: none - + StateMachineArn: Type: String Default: none @@ -20,21 +20,21 @@ Parameters: Type: CommaDelimitedList Description: A list of additional policies to attach the state machines role (comma delimited). Default: none - + LogRetentionInDays: Type: Number - + CloudWatchKMSKeyId: Type: String Default: none - + EnableSplunk: Type: String - + SplunkSubscriptionFilterRole: Type: String Default: none - + SplunkDeliveryStreamArn: Type: String Default: none @@ -57,7 +57,7 @@ Resources: - states:StartExecution Resource: - !Ref StateMachineArn - + StateMachineRole: Type: AWS::IAM::Role Properties: @@ -75,15 +75,15 @@ Resources: StringEquals: aws:SourceAccount: !Ref AWS::AccountId ManagedPolicyArns: !Split - - ',' + - "," - !Join - - ',' + - "," - - !Ref StateMachineManagedPolicy - !ImportValue account-resources:CloudwatchEncryptionKMSPolicyArn - !Join - - ',' + - "," - !Ref AdditionalPolicies - + StateMachineManagedPolicy: Type: AWS::IAM::ManagedPolicy Properties: @@ -108,17 +108,17 @@ Resources: StateMachineLogGroup: Type: AWS::Logs::LogGroup Properties: - LogGroupName: !Sub /aws/stepfunctions/${StateMachineName} + LogGroupName: !Sub /aws/stepfunctions/new${StateMachineName} RetentionInDays: !Ref LogRetentionInDays KmsKeyId: !Ref CloudWatchKMSKeyId - + StateMachineSplunkSubscriptionFilter: Condition: ShouldUseSplunk Type: AWS::Logs::SubscriptionFilter Properties: RoleArn: !Ref SplunkSubscriptionFilterRole LogGroupName: !Ref StateMachineLogGroup - FilterPattern: '' + FilterPattern: "" DestinationArn: !Ref SplunkDeliveryStreamArn Outputs: From fb3c8dad2ce7179d1360695a50515619ca66f584 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:25:13 +0000 Subject: [PATCH 17/33] more log policies for state machine --- SAMtemplates/state_machines/state_machine_resources.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SAMtemplates/state_machines/state_machine_resources.yaml b/SAMtemplates/state_machines/state_machine_resources.yaml index d590ca1b7..af0a81c50 100644 --- a/SAMtemplates/state_machines/state_machine_resources.yaml +++ b/SAMtemplates/state_machines/state_machine_resources.yaml @@ -103,12 +103,16 @@ Resources: - logs:ListLogDeliveries - logs:CreateLogDelivery - logs:GetLogDelivery + - logs:UpdateLogDelivery + - logs:DeleteLogDelivery + - logs:PutResourcePolicy + - logs:DescribeResourcePolicies Resource: "*" StateMachineLogGroup: Type: AWS::Logs::LogGroup Properties: - LogGroupName: !Sub /aws/stepfunctions/new${StateMachineName} + LogGroupName: !Sub /aws/stepfunctions/${StateMachineName} RetentionInDays: !Ref LogRetentionInDays KmsKeyId: !Ref CloudWatchKMSKeyId From da73b4902b77628373a20d6aa44693a0bdc8017c Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:31:19 +0000 Subject: [PATCH 18/33] add export for GSUL arn --- SAMtemplates/functions/main.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SAMtemplates/functions/main.yaml b/SAMtemplates/functions/main.yaml index dd322f8a6..cc036d7af 100644 --- a/SAMtemplates/functions/main.yaml +++ b/SAMtemplates/functions/main.yaml @@ -121,3 +121,13 @@ Outputs: UpdatePrescriptionStatusFunctionArn: Description: The function ARN of the UpdatePrescriptionStatus lambda Value: !GetAtt UpdatePrescriptionStatus.Arn + + GSULName: + Description: The function name of the GUSL + Value: !Ref GUSL + + GSULArn: + Description: The function ARN of the GUSL + Value: !GetAtt GUSL.Arn + Export: + Name: !Sub ${StackName}:functions:GSUL:GSULArn From 2805b7987b809520c254bbf4f950ae4d33549ed7 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:49:59 +0000 Subject: [PATCH 19/33] use correct index name --- packages/gsul/src/getStatusUpdates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index 3ca8440bd..ffe8420cb 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -26,7 +26,7 @@ const lambdaHandler = async (event: requestType): Promise => { // create query for each prescription and ods code passed in const queryParam : QueryCommandInput = { TableName: tableName, - IndexName: "PrescriptionIDIndex", + IndexName: "PharmacyODSCodePrescriptionIDIndex", KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", ExpressionAttributeValues: { ":inputPharmacyODSCode": prescription.odsCode, From 9ab16e408ac6a435daccc560d5280fefadc07a29 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Mon, 15 Apr 2024 18:44:11 +0000 Subject: [PATCH 20/33] add prettier-eslint --- package-lock.json | 307 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 308 insertions(+) diff --git a/package-lock.json b/package-lock.json index 001081c04..dd5bcc157 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "jest-junit": "^16.0.0", "license-checker": "^25.0.1", "prettier": "^3.2.5", + "prettier-eslint": "^16.3.0", "semantic-release": "^23.0.8", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", @@ -4539,6 +4540,15 @@ "dev": true, "license": "MIT" }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/compare-func": { "version": "2.0.0", "dev": true, @@ -4888,6 +4898,12 @@ "version": "1.0.0", "license": "MIT" }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, "node_modules/doctrine": { "version": "3.0.0", "dev": true, @@ -5987,6 +6003,27 @@ "uglify-js": "^3.1.4" } }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "license": "MIT", @@ -7427,6 +7464,93 @@ "dev": true, "license": "MIT" }, + "node_modules/loglevel": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz", + "integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-colored-level-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz", + "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "loglevel": "^1.4.1" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel-colored-level-prefix/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/lower-case": { "version": "2.0.2", "license": "MIT", @@ -11050,6 +11174,159 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-eslint": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-16.3.0.tgz", + "integrity": "sha512-Lh102TIFCr11PJKUMQ2kwNmxGhTsv/KzUg9QYF2Gkw259g/kPgndZDWavk7/ycbRvj2oz4BPZ1gCU8bhfZH/Xg==", + "dev": true, + "dependencies": { + "@typescript-eslint/parser": "^6.7.5", + "common-tags": "^1.4.0", + "dlv": "^1.1.0", + "eslint": "^8.7.0", + "indent-string": "^4.0.0", + "lodash.merge": "^4.6.0", + "loglevel-colored-level-prefix": "^1.0.0", + "prettier": "^3.0.1", + "pretty-format": "^29.7.0", + "require-relative": "^0.8.7", + "typescript": "^5.2.2", + "vue-eslint-parser": "^9.1.0" + }, + "engines": { + "node": ">=16.10.0" + }, + "peerDependencies": { + "prettier-plugin-svelte": "^3.0.0", + "svelte-eslint-parser": "*" + }, + "peerDependenciesMeta": { + "prettier-plugin-svelte": { + "optional": true + }, + "svelte-eslint-parser": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prettier-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/prettier-eslint/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/prettier-linter-helpers": { "version": "1.0.0", "dev": true, @@ -11476,6 +11753,12 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.8", "dev": true, @@ -12967,6 +13250,30 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vue-eslint-parser": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", + "integrity": "sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/walker": { "version": "1.0.8", "dev": true, diff --git a/package.json b/package.json index 04e088ca5..1e45aed1d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "jest-junit": "^16.0.0", "license-checker": "^25.0.1", "prettier": "^3.2.5", + "prettier-eslint": "^16.3.0", "semantic-release": "^23.0.8", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", From af7c80054dca0052fc9ff0ba0bf41a4974f783cb Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:24:29 +0000 Subject: [PATCH 21/33] trigger build From 87ced49235e4f8d9105980bf270c5ee303dc1206 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Wed, 17 Apr 2024 18:45:50 +0000 Subject: [PATCH 22/33] use pr fhir validator --- SAMtemplates/state_machines/main.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SAMtemplates/state_machines/main.yaml b/SAMtemplates/state_machines/main.yaml index fb9e6a3fc..a7aadf418 100644 --- a/SAMtemplates/state_machines/main.yaml +++ b/SAMtemplates/state_machines/main.yaml @@ -33,7 +33,7 @@ Resources: DefinitionSubstitutions: FhirValidationFunctionArn: !Join - ":" - - - !ImportValue fhir-validator:FHIRValidatorUKCoreLambdaArn + - - !ImportValue fhir-validator-pr-76:FHIRValidatorUKCoreLambdaArn - snap UpdatePrescriptionStatusFunctionArn: !Sub ${UpdatePrescriptionStatusFunctionArn}:$LATEST Logging: @@ -56,7 +56,7 @@ Resources: AdditionalPolicies: !Join - "," - - Fn::ImportValue: !Sub ${StackName}:functions:${UpdatePrescriptionStatusFunctionName}:ExecuteLambdaPolicyArn - - !ImportValue fhir-validator:FHIRValidatorUKCoreExecuteLambdaPolicyArn + - !ImportValue fhir-validator-pr-76:FHIRValidatorUKCoreExecuteLambdaPolicyArn LogRetentionInDays: !Ref LogRetentionInDays CloudWatchKMSKeyId: !ImportValue account-resources:CloudwatchLogsKmsKeyArn EnableSplunk: !Ref EnableSplunk From 0bd5fb6a7bcfede9310ef13189ad6b370bc6e518 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Wed, 17 Apr 2024 20:16:41 +0000 Subject: [PATCH 23/33] pass full request to validator --- .../state_machines/UpdatePrescriptionStatusStateMachine.asl.json | 1 - 1 file changed, 1 deletion(-) diff --git a/SAMtemplates/state_machines/UpdatePrescriptionStatusStateMachine.asl.json b/SAMtemplates/state_machines/UpdatePrescriptionStatusStateMachine.asl.json index b7c59489f..cbb9d1d54 100644 --- a/SAMtemplates/state_machines/UpdatePrescriptionStatusStateMachine.asl.json +++ b/SAMtemplates/state_machines/UpdatePrescriptionStatusStateMachine.asl.json @@ -15,7 +15,6 @@ "NumberFailedValidation.$": "States.ArrayLength($.Payload.issue[?(@.severity ==error)])" }, "Next": "Do FHIR Validation Errors Exist", - "InputPath": "$.body", "Catch": [ { "ErrorEquals": [ From 8684086e8b9ee840fea4da70db290951107e108a Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 18 Apr 2024 06:45:00 +0000 Subject: [PATCH 24/33] rollback tests --- .../UpdatePrescriptionStatusStateMachine.asl.json | 1 + SAMtemplates/state_machines/main.yaml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SAMtemplates/state_machines/UpdatePrescriptionStatusStateMachine.asl.json b/SAMtemplates/state_machines/UpdatePrescriptionStatusStateMachine.asl.json index cbb9d1d54..b7c59489f 100644 --- a/SAMtemplates/state_machines/UpdatePrescriptionStatusStateMachine.asl.json +++ b/SAMtemplates/state_machines/UpdatePrescriptionStatusStateMachine.asl.json @@ -15,6 +15,7 @@ "NumberFailedValidation.$": "States.ArrayLength($.Payload.issue[?(@.severity ==error)])" }, "Next": "Do FHIR Validation Errors Exist", + "InputPath": "$.body", "Catch": [ { "ErrorEquals": [ diff --git a/SAMtemplates/state_machines/main.yaml b/SAMtemplates/state_machines/main.yaml index a7aadf418..fb9e6a3fc 100644 --- a/SAMtemplates/state_machines/main.yaml +++ b/SAMtemplates/state_machines/main.yaml @@ -33,7 +33,7 @@ Resources: DefinitionSubstitutions: FhirValidationFunctionArn: !Join - ":" - - - !ImportValue fhir-validator-pr-76:FHIRValidatorUKCoreLambdaArn + - - !ImportValue fhir-validator:FHIRValidatorUKCoreLambdaArn - snap UpdatePrescriptionStatusFunctionArn: !Sub ${UpdatePrescriptionStatusFunctionArn}:$LATEST Logging: @@ -56,7 +56,7 @@ Resources: AdditionalPolicies: !Join - "," - - Fn::ImportValue: !Sub ${StackName}:functions:${UpdatePrescriptionStatusFunctionName}:ExecuteLambdaPolicyArn - - !ImportValue fhir-validator-pr-76:FHIRValidatorUKCoreExecuteLambdaPolicyArn + - !ImportValue fhir-validator:FHIRValidatorUKCoreExecuteLambdaPolicyArn LogRetentionInDays: !Ref LogRetentionInDays CloudWatchKMSKeyId: !ImportValue account-resources:CloudwatchLogsKmsKeyArn EnableSplunk: !Ref EnableSplunk From 21432f41e0b3343878da9ef5f816f987a53cf3b2 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 18 Apr 2024 07:05:26 +0000 Subject: [PATCH 25/33] log correlation keys --- .../src/updatePrescriptionStatus.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts b/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts index 3c7b8439b..46b23e37f 100644 --- a/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts +++ b/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts @@ -32,16 +32,26 @@ export interface DataItem { } const lambdaHandler = async (event: APIGatewayProxyEvent): Promise => { + logger.appendKeys({ + "nhsd-correlation-id": event.headers["nhsd-correlation-id"], + "nhsd-request-id": event.headers["nhsd-request-id"], + "x-correlation-id": event.headers["x-correlation-id"], + "apigw-request-id": event.requestContext.requestId + }) let responseEntries: Array = [] const xRequestID = getXRequestID(event, responseEntries) + if (!xRequestID) { return response(400, responseEntries) } + logger.appendKeys({ + "x-request-id": xRequestID + }) const requestBody = event.body const requestBundle = castEventBody(requestBody, responseEntries) - if(!requestBundle) { + if (!requestBundle) { return response(400, responseEntries) } From c1e08f0532fe118ec01813af5f84fe816acb52ee Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 18 Apr 2024 08:09:05 +0000 Subject: [PATCH 26/33] rename lambda --- SAMtemplates/functions/main.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/SAMtemplates/functions/main.yaml b/SAMtemplates/functions/main.yaml index cc036d7af..69bc3fc57 100644 --- a/SAMtemplates/functions/main.yaml +++ b/SAMtemplates/functions/main.yaml @@ -74,13 +74,13 @@ Resources: SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole SplunkDeliveryStreamArn: !ImportValue lambda-resources:SplunkDeliveryStream - GUSL: + GetStatusUpdates: Type: AWS::Serverless::Function Properties: FunctionName: !Sub ${StackName}-GetStatusUpdates CodeUri: ../../packages Handler: getStatusUpdates.handler - Role: !GetAtt GSULResources.Outputs.LambdaRoleArn + Role: !GetAtt GetStatusUpdatesResources.Outputs.LambdaRoleArn Environment: Variables: TABLE_NAME: !Ref PrescriptionStatusUpdatesTableName @@ -95,7 +95,7 @@ Resources: EntryPoints: - gsul/src/getStatusUpdates.ts - GSULResources: + GetStatusUpdatesResources: Type: AWS::Serverless::Application Properties: Location: lambda_resources.yaml @@ -122,12 +122,12 @@ Outputs: Description: The function ARN of the UpdatePrescriptionStatus lambda Value: !GetAtt UpdatePrescriptionStatus.Arn - GSULName: - Description: The function name of the GUSL - Value: !Ref GUSL + GetStatusUpdatesFunctionName: + Description: The function name of the GetStatusUpdates lambda + Value: !Ref GetStatusUpdates - GSULArn: - Description: The function ARN of the GUSL - Value: !GetAtt GUSL.Arn + GetStatusUpdatesFunctionArn: + Description: The function ARN of the GetStatusUpdates lambda + Value: !GetAtt GetStatusUpdates.Arn Export: - Name: !Sub ${StackName}:functions:GSUL:GSULArn + Name: !Sub ${StackName}:functions:${GetStatusUpdates}:FunctionArn From fa2e468e65df013b6592676e062f776ce30a9353 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Thu, 18 Apr 2024 08:21:57 +0000 Subject: [PATCH 27/33] correct export name --- SAMtemplates/functions/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SAMtemplates/functions/main.yaml b/SAMtemplates/functions/main.yaml index 69bc3fc57..5cefcbea6 100644 --- a/SAMtemplates/functions/main.yaml +++ b/SAMtemplates/functions/main.yaml @@ -130,4 +130,4 @@ Outputs: Description: The function ARN of the GetStatusUpdates lambda Value: !GetAtt GetStatusUpdates.Arn Export: - Name: !Sub ${StackName}:functions:${GetStatusUpdates}:FunctionArn + Name: !Sub ${StackName}:functions:GetStatusUpdates:FunctionArn From cded1ca07b3da756053688019427a40275754b87 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Tue, 23 Apr 2024 09:14:56 +0000 Subject: [PATCH 28/33] changes following feedback --- package-lock.json | 3 + packages/gsul/src/dynamoDBclient.ts | 70 ++++---- packages/gsul/src/getStatusUpdates.ts | 139 +++++++--------- packages/gsul/tests/testBuildResult.test.ts | 150 +++++++++++++++++ packages/gsul/tests/testBuildResults.test.ts | 149 ----------------- .../gsul/tests/testRunDynamoDBQueries.test.ts | 151 +++++++----------- 6 files changed, 311 insertions(+), 351 deletions(-) create mode 100644 packages/gsul/tests/testBuildResult.test.ts delete mode 100644 packages/gsul/tests/testBuildResults.test.ts diff --git a/package-lock.json b/package-lock.json index 76b110957..dd1f98b24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3802,6 +3802,7 @@ }, "node_modules/ajv": { "version": "6.12.6", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -5555,6 +5556,7 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -7151,6 +7153,7 @@ }, "node_modules/json-schema-traverse": { "version": "0.4.1", + "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { diff --git a/packages/gsul/src/dynamoDBclient.ts b/packages/gsul/src/dynamoDBclient.ts index c27871d44..f608d2ac0 100644 --- a/packages/gsul/src/dynamoDBclient.ts +++ b/packages/gsul/src/dynamoDBclient.ts @@ -1,13 +1,29 @@ - import {DynamoDBDocumentClient, QueryCommand, QueryCommandInput} from "@aws-sdk/lib-dynamodb" import {Logger} from "@aws-lambda-powertools/logger" -import{DynamoDBResult} from "./schema/result.ts" +import {DynamoDBResult} from "./schema/result.ts" import {NativeAttributeValue} from "@aws-sdk/util-dynamodb" +import {inputPrescriptionType} from "./schema/request.ts" -export function runDynamoDBQueries (queryParams: Array, - docClient: DynamoDBDocumentClient, - logger: Logger): Array>> { +const tableName = process.env.TABLE_NAME + +export function createDynamoDBQuery(prescription: inputPrescriptionType) { + const queryParam: QueryCommandInput = { + TableName: tableName, + IndexName: "PharmacyODSCodePrescriptionIDIndex", + KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", + ExpressionAttributeValues: { + ":inputPharmacyODSCode": prescription.odsCode, + ":inputPrescriptionID": prescription.prescriptionID + } + } + return queryParam +} +export async function runDynamoDBQuery( + query: QueryCommandInput, + docClient: DynamoDBDocumentClient, + logger: Logger +): Promise> { // helper function to deal with pagination of results from dynamodb const getAllData = async (query: QueryCommandInput) => { const _getAllData = async (query: QueryCommandInput, startKey: Record) => { @@ -28,34 +44,22 @@ export function runDynamoDBQueries (queryParams: Array, return rows } - const queryResultsTasks: Array>> = queryParams.map(async (query) => { - // run each query - const items = await getAllData(query) - - if (items.length !== 0) { - - const response: Array = items.map((singleUpdate) => { - const result: DynamoDBResult = { - prescriptionID: String(singleUpdate.PrescriptionID), - itemId: String(singleUpdate.LineItemID), - latestStatus: String(singleUpdate.Status), - isTerminalState: String(singleUpdate.TerminalStatus), - lastUpdateDateTime: String(singleUpdate.LastModified) - } - return result - }) + const items = await getAllData(query) - return response - } - const result: Array = [{ - prescriptionID: undefined, - itemId: undefined, - latestStatus: undefined, - isTerminalState: undefined, - lastUpdateDateTime: undefined - }] - return result - }) + if (items.length !== 0) { + const response: Array = items.map((singleUpdate) => { + const result: DynamoDBResult = { + prescriptionID: String(singleUpdate.PrescriptionID), + itemId: String(singleUpdate.LineItemID), + latestStatus: String(singleUpdate.Status), + isTerminalState: String(singleUpdate.TerminalStatus), + lastUpdateDateTime: String(singleUpdate.LastModified) + } + return result + }) + return response + } + const result: Array = [] - return queryResultsTasks + return result } diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index ffe8420cb..8ff474f40 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -2,13 +2,13 @@ import {Logger} from "@aws-lambda-powertools/logger" import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" import {DynamoDBClient} from "@aws-sdk/client-dynamodb" -import {DynamoDBDocumentClient, QueryCommandInput} from "@aws-sdk/lib-dynamodb" +import {DynamoDBDocumentClient} from "@aws-sdk/lib-dynamodb" import middy from "@middy/core" import inputOutputLogger from "@middy/input-output-logger" import validator from "@middy/validator" import {transpileSchema} from "@middy/validator/transpile" import {errorHandler} from "./errorHandler.ts" -import {runDynamoDBQueries} from "./dynamoDBclient.ts" +import {createDynamoDBQuery, runDynamoDBQuery} from "./dynamoDBclient.ts" import {requestSchema, requestType, inputPrescriptionType} from "./schema/request.ts" import {responseType, outputPrescriptionType, itemType} from "./schema/response.ts" import {DynamoDBResult} from "./schema/result.ts" @@ -16,104 +16,83 @@ import {DynamoDBResult} from "./schema/result.ts" const logger = new Logger({serviceName: "GSUL"}) const client = new DynamoDBClient({region: "eu-west-2"}) const docClient = DynamoDBDocumentClient.from(client) -const tableName = process.env.TABLE_NAME const lambdaHandler = async (event: requestType): Promise => { // there are deliberately no try..catch blocks in this as any errors are caught by custom middy error handler // and an error response is sent - const queryParams = event.prescriptions.map((prescription) => { - // create query for each prescription and ods code passed in - const queryParam : QueryCommandInput = { - TableName: tableName, - IndexName: "PharmacyODSCodePrescriptionIDIndex", - KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", - ExpressionAttributeValues: { - ":inputPharmacyODSCode": prescription.odsCode, - ":inputPrescriptionID": prescription.prescriptionID - } - } - return queryParam + // this is an async map so it returns an array of promises + const itemResults = event.prescriptions.map(async (prescription) => { + const query = createDynamoDBQuery(prescription) + const queryResult = await runDynamoDBQuery(query, docClient, logger) + const result = buildResult(prescription, queryResult) + return result }) - // run the dynamodb queries - const queryResultsTasks = runDynamoDBQueries(queryParams, docClient, logger) - - // get all the query results - const queryResults = await Promise.all(queryResultsTasks) - - // queryResults is an array of arrays - // the first array is for each prescription id passed in as we have created a query for each prescription - // and the second array is the items for that prescription id - // we use flat() to just get an array of items which we can pass to buildResults - const itemResults = buildResults(event.prescriptions, queryResults.flat()) - + // wait for all the promises to complete + const finalResults = await Promise.all(itemResults) const response = { - "schemaVersion": 1, - "isSuccess": true, - "prescriptions": itemResults + schemaVersion: 1, + isSuccess: true, + prescriptions: finalResults } return response } -export const buildResults = (inputPrescriptions: Array, - queryResults: Array): Array => { - // for each prescription id passed in build up a response - const itemResults = inputPrescriptions.map((prescription) => { - const items = queryResults.filter((queryResult) => { - // see if we have any results for the prescription id - return queryResult?.prescriptionID === prescription.prescriptionID - }) - // if we have results then populate the response - if (items.length > 0) { - // get the latest update per item id - - // first get the unique item ids - const uniqueItemIds = [...new Set(items.map((item) => { - return item.itemId - }))] - - // now get an array of the latest updates for each unique item id - const latestUpdates = uniqueItemIds.map((itemId)=> { - - // get all the updates for this prescription id and item id - const matchedItems = items.filter((item) => { - return item.prescriptionID === prescription.prescriptionID && item.itemId === itemId +export const buildResult = ( + inputPrescription: inputPrescriptionType, + items: Array +): outputPrescriptionType => { + // if we have results then populate the response + if (items.length > 0) { + // we need to get the latest update per item id + // first get the unique item ids + const uniqueItemIds = [ + ...new Set( + items.map((item) => { + return item.itemId }) + ) + ] - // now just get the latest update - const latestUpdate = matchedItems.reduce((prev, current) => { - return (prev && Date.parse(prev.lastUpdateDateTime) > Date.parse(current.lastUpdateDateTime)) ? prev : current - }) - - return latestUpdate + // now get an array of the latest updates for each unique item id + const latestUpdates = uniqueItemIds.map((itemId) => { + // get all the updates for this item id + const matchedItems = items.filter((item) => { + return item.itemId === itemId }) - const responseItems = latestUpdates.map((item) => { - const returnItem: itemType = { - "itemId": String(item.itemId), - "latestStatus": String(item.latestStatus), - "isTerminalState": String(item.isTerminalState), - "lastUpdateDateTime": String(item.lastUpdateDateTime) - } - return returnItem + // now just get the latest update + const latestUpdate = matchedItems.reduce((prev, current) => { + return prev && Date.parse(prev.lastUpdateDateTime) > Date.parse(current.lastUpdateDateTime) ? prev : current }) - const result: outputPrescriptionType = { - "prescriptionID": prescription.prescriptionID, - "onboarded": true, - "items": responseItems + + return latestUpdate + }) + + const responseItems = latestUpdates.map((item) => { + const returnItem: itemType = { + itemId: String(item.itemId), + latestStatus: String(item.latestStatus), + isTerminalState: String(item.isTerminalState), + lastUpdateDateTime: String(item.lastUpdateDateTime) } - return result - } - // we have no results returned so return an empty array of items - const result = { - "prescriptionID": prescription.prescriptionID, - "onboarded": true, - "items": [] + return returnItem + }) + const result: outputPrescriptionType = { + prescriptionID: inputPrescription.prescriptionID, + onboarded: true, + items: responseItems } return result - }) - return itemResults + } + // we have no results returned so return an empty array of items + const result = { + prescriptionID: inputPrescription.prescriptionID, + onboarded: false, + items: [] + } + return result } export const handler = middy(lambdaHandler) diff --git a/packages/gsul/tests/testBuildResult.test.ts b/packages/gsul/tests/testBuildResult.test.ts new file mode 100644 index 000000000..c8f5ba04d --- /dev/null +++ b/packages/gsul/tests/testBuildResult.test.ts @@ -0,0 +1,150 @@ +import {buildResult} from "../src/getStatusUpdates" +import {inputPrescriptionType} from "../src/schema/request" +import {DynamoDBResult} from "../src/schema/result" +import {outputPrescriptionType} from "../src/schema/response" + +type scenariosType = { + scenarioDescription: string + inputPrescriptions: inputPrescriptionType + queryResults: Array + expectedResult: outputPrescriptionType +} +const scenarios: Array = [ + { + scenarioDescription: "should return correct data when a matched prescription found", + inputPrescriptions: { + prescriptionID: "abc", + odsCode: "123" + }, + queryResults: [ + { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + } + ], + expectedResult: { + prescriptionID: "abc", + onboarded: true, + items: [ + { + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + } + ] + } + }, + { + scenarioDescription: "should return no items when empty item status are found", + inputPrescriptions: { + prescriptionID: "abc", + odsCode: "123" + }, + queryResults: [], + expectedResult: { + prescriptionID: "abc", + onboarded: false, + items: [] + } + }, + { + scenarioDescription: "should return latest data when a multiple updates found", + inputPrescriptions: { + prescriptionID: "abc", + odsCode: "123" + }, + queryResults: [ + { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "early_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }, + { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1971-01-01T00:00:00Z" + } + ], + expectedResult: { + prescriptionID: "abc", + onboarded: true, + items: [ + { + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1971-01-01T00:00:00Z" + } + ] + } + }, + { + scenarioDescription: "should return correct data for multiple items", + inputPrescriptions: { + prescriptionID: "abc", + odsCode: "123" + }, + queryResults: [ + { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "item_1_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }, + { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_item_1_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1972-01-01T00:00:00Z" + }, + { + prescriptionID: "abc", + itemId: "item_2", + latestStatus: "item_2_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1971-01-01T00:00:00Z" + }, + { + prescriptionID: "abc", + itemId: "item_2", + latestStatus: "early_item_2_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + } + ], + expectedResult: { + prescriptionID: "abc", + onboarded: true, + items: [ + { + itemId: "item_1", + latestStatus: "latest_item_1_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1972-01-01T00:00:00Z" + }, + { + itemId: "item_2", + latestStatus: "item_2_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1971-01-01T00:00:00Z" + } + ] + } + } +] +describe("Unit tests for buildResults", () => { + it.each(scenarios)("$scenarioDescription", ({inputPrescriptions, queryResults, expectedResult}) => { + const result = buildResult(inputPrescriptions, queryResults) + expect(result).toMatchObject(expectedResult) + }) +}) diff --git a/packages/gsul/tests/testBuildResults.test.ts b/packages/gsul/tests/testBuildResults.test.ts deleted file mode 100644 index 58ed9a053..000000000 --- a/packages/gsul/tests/testBuildResults.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import {buildResults} from "../src/getStatusUpdates" -import {inputPrescriptionType} from "../src/schema/request" -import {DynamoDBResult} from "../src/schema/result" -import {outputPrescriptionType} from "../src/schema/response" - -type scenariosType = { - scenarioDescription: string; - inputPrescriptions: Array; - queryResults: Array, - expectedResult: Array; -} -const scenarios: Array = [ - { - scenarioDescription: "should return correct data when a matched prescription found", - inputPrescriptions: [{ - prescriptionID: "abc", - odsCode: "123" - }], - queryResults: [{ - prescriptionID: "abc", - itemId: "item_1", - latestStatus: "latest_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }], - expectedResult: [{ - prescriptionID: "abc", - onboarded: true, - items: [{ - itemId: "item_1", - latestStatus: "latest_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }] - }] - }, { - scenarioDescription: "should return no items when empty item status are found", - inputPrescriptions: [{ - prescriptionID: "abc", - odsCode: "123" - }], - queryResults: [], - expectedResult: [{ - prescriptionID: "abc", - onboarded: true, - items: [] - }] - }, { - scenarioDescription: "should return no items when item status are found for different prescription", - inputPrescriptions: [{ - prescriptionID: "abc", - odsCode: "123" - }], - queryResults: [{ - prescriptionID: "def", - itemId: "item_1", - latestStatus: "latest_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }], - expectedResult: [{ - prescriptionID: "abc", - onboarded: true, - items: [] - }] - }, { - scenarioDescription: "should return latest data when a multiple updates found", - inputPrescriptions: [{ - prescriptionID: "abc", - odsCode: "123" - }], - queryResults: [{ - prescriptionID: "abc", - itemId: "item_1", - latestStatus: "early_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }, { - prescriptionID: "abc", - itemId: "item_1", - latestStatus: "latest_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1971-01-01T00:00:00Z" - }], - expectedResult: [{ - prescriptionID: "abc", - onboarded: true, - items: [{ - itemId: "item_1", - latestStatus: "latest_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1971-01-01T00:00:00Z" - }] - }] - }, { - scenarioDescription: "should return correct data for multiple items", - inputPrescriptions: [{ - prescriptionID: "abc", - odsCode: "123" - }], - queryResults: [{ - prescriptionID: "abc", - itemId: "item_1", - latestStatus: "item_1_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }, { - prescriptionID: "abc", - itemId: "item_1", - latestStatus: "latest_item_1_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1972-01-01T00:00:00Z" - }, { - prescriptionID: "abc", - itemId: "item_2", - latestStatus: "item_2_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1971-01-01T00:00:00Z" - }, { - prescriptionID: "abc", - itemId: "item_2", - latestStatus: "early_item_2_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }], - expectedResult: [{ - prescriptionID: "abc", - onboarded: true, - items: [{ - itemId: "item_1", - latestStatus: "latest_item_1_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1972-01-01T00:00:00Z" - }, { - itemId: "item_2", - latestStatus: "item_2_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1971-01-01T00:00:00Z" - }] - }] - } -] -describe("Unit tests for buildResults", () => { - it.each(scenarios)("$scenarioDescription", ({inputPrescriptions, queryResults, expectedResult}) => { - const result = buildResults(inputPrescriptions, queryResults) - expect(result).toMatchObject(expectedResult) - }) - -}) diff --git a/packages/gsul/tests/testRunDynamoDBQueries.test.ts b/packages/gsul/tests/testRunDynamoDBQueries.test.ts index ac14a0bf8..380bfca51 100644 --- a/packages/gsul/tests/testRunDynamoDBQueries.test.ts +++ b/packages/gsul/tests/testRunDynamoDBQueries.test.ts @@ -1,6 +1,6 @@ import {QueryCommandInput, DynamoDBDocumentClient} from "@aws-sdk/lib-dynamodb" import {DynamoDBClient} from "@aws-sdk/client-dynamodb" -import {runDynamoDBQueries} from "../src/dynamoDBclient" +import {runDynamoDBQuery} from "../src/dynamoDBclient" import {Logger} from "@aws-lambda-powertools/logger" import { expect, @@ -20,19 +20,21 @@ describe("testing dynamoDBClient", () => { jest.clearAllMocks() const mockReply = { Count: 1, - Items: [{ - PrescriptionID: "abc", - LineItemID: "item_1", - Status: "latest_status", - TerminalStatus: "is_terminal_status", - LastModified: "1970-01-01T00:00:00Z" - }] + Items: [ + { + PrescriptionID: "abc", + LineItemID: "item_1", + Status: "latest_status", + TerminalStatus: "is_terminal_status", + LastModified: "1970-01-01T00:00:00Z" + } + ] } jest.spyOn(DynamoDBDocumentClient.prototype, "send").mockResolvedValue(mockReply as never) }) it("should call dynamo once and return expected items", async () => { - const queryParams : Array = [{ + const queryParams: QueryCommandInput = { TableName: "tableName", IndexName: "indexName", KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", @@ -40,55 +42,20 @@ describe("testing dynamoDBClient", () => { ":inputPharmacyODSCode": "odsCode", ":inputPrescriptionID": "prescriptionID" } - }] - const queryResultsTasks = runDynamoDBQueries(queryParams, docClient, logger) - const queryResults = await Promise.all(queryResultsTasks) - const itemResults = queryResults.flat() - expect(DynamoDBDocumentClient.prototype.send).toHaveBeenCalledTimes(1) - expect(itemResults).toMatchObject([{ - prescriptionID: "abc", - itemId: "item_1", - latestStatus: "latest_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }]) - }) + } - it("should call dynamo twice and return expected items", async () => { - const queryParams : Array = [{ - TableName: "tableName", - IndexName: "indexName", - KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", - ExpressionAttributeValues: { - ":inputPharmacyODSCode": "odsCode", - ":inputPrescriptionID": "prescriptionID" - } - }, { - TableName: "tableName", - IndexName: "indexName", - KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", - ExpressionAttributeValues: { - ":inputPharmacyODSCode": "odsCode", - ":inputPrescriptionID": "prescriptionID" + const queryResults = await runDynamoDBQuery(queryParams, docClient, logger) + + expect(DynamoDBDocumentClient.prototype.send).toHaveBeenCalledTimes(1) + expect(queryResults).toMatchObject([ + { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "latest_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" } - }] - const queryResultsTasks = runDynamoDBQueries(queryParams, docClient, logger) - const queryResults = await Promise.all(queryResultsTasks) - const itemResults = queryResults.flat() - expect(DynamoDBDocumentClient.prototype.send).toHaveBeenCalledTimes(2) - expect(itemResults).toMatchObject([{ - prescriptionID: "abc", - itemId: "item_1", - latestStatus: "latest_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }, { - prescriptionID: "abc", - itemId: "item_1", - latestStatus: "latest_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }]) + ]) }) }) @@ -98,13 +65,15 @@ describe("testing pagination in dynamoDBClient", () => { jest.clearAllMocks() const mockFirstReply = { Count: 1, - Items: [{ - PrescriptionID: "abc", - LineItemID: "item_1", - Status: "first_status", - TerminalStatus: "is_terminal_status", - LastModified: "1970-01-01T00:00:00Z" - }], + Items: [ + { + PrescriptionID: "abc", + LineItemID: "item_1", + Status: "first_status", + TerminalStatus: "is_terminal_status", + LastModified: "1970-01-01T00:00:00Z" + } + ], LastEvaluatedKey: { PharmacyODSCode: "C9Z1O", RequestID: "d90b88b-9cc8-4b70-9d9f-0144adcc38cc", @@ -113,21 +82,24 @@ describe("testing pagination in dynamoDBClient", () => { } const mockSecondReply = { Count: 1, - Items: [{ - PrescriptionID: "abc", - LineItemID: "item_1", - Status: "second_status", - TerminalStatus: "is_terminal_status", - LastModified: "1970-01-01T00:00:00Z" - }] + Items: [ + { + PrescriptionID: "abc", + LineItemID: "item_1", + Status: "second_status", + TerminalStatus: "is_terminal_status", + LastModified: "1970-01-01T00:00:00Z" + } + ] } - jest.spyOn(DynamoDBDocumentClient.prototype, "send") + jest + .spyOn(DynamoDBDocumentClient.prototype, "send") .mockResolvedValueOnce(mockFirstReply as never) .mockResolvedValueOnce(mockSecondReply as never) }) it("should handle pagination", async () => { - const queryParams : Array = [{ + const queryParams: QueryCommandInput = { TableName: "tableName", IndexName: "indexName", KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", @@ -135,24 +107,25 @@ describe("testing pagination in dynamoDBClient", () => { ":inputPharmacyODSCode": "odsCode", ":inputPrescriptionID": "prescriptionID" } - }] - const queryResultsTasks = runDynamoDBQueries(queryParams, docClient, logger) - const queryResults = await Promise.all(queryResultsTasks) - const itemResults = queryResults.flat() + } + const queryResults = await runDynamoDBQuery(queryParams, docClient, logger) - expect(itemResults).toMatchObject([{ - prescriptionID: "abc", - itemId: "item_1", - latestStatus: "first_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }, { - prescriptionID: "abc", - itemId: "item_1", - latestStatus: "second_status", - isTerminalState: "is_terminal_status", - lastUpdateDateTime: "1970-01-01T00:00:00Z" - }]) + expect(queryResults).toMatchObject([ + { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "first_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + }, + { + prescriptionID: "abc", + itemId: "item_1", + latestStatus: "second_status", + isTerminalState: "is_terminal_status", + lastUpdateDateTime: "1970-01-01T00:00:00Z" + } + ]) expect(DynamoDBDocumentClient.prototype.send).toHaveBeenCalledTimes(2) }) }) From 12a817a6965969e05f5a454c33f19712c45cb5e3 Mon Sep 17 00:00:00 2001 From: MatthewPopat-NHS Date: Tue, 23 Apr 2024 11:08:36 +0000 Subject: [PATCH 29/33] Simplified GSUL dedup logic and dynamo interface. --- packages/gsul/src/dynamoDBclient.ts | 59 ++++++--------- packages/gsul/src/getStatusUpdates.ts | 73 +++++-------------- packages/gsul/src/schema/result.ts | 9 --- packages/gsul/tests/testBuildResult.test.ts | 12 +-- .../gsul/tests/testRunDynamoDBQueries.test.ts | 34 +-------- 5 files changed, 48 insertions(+), 139 deletions(-) delete mode 100644 packages/gsul/src/schema/result.ts diff --git a/packages/gsul/src/dynamoDBclient.ts b/packages/gsul/src/dynamoDBclient.ts index f608d2ac0..23ca1122e 100644 --- a/packages/gsul/src/dynamoDBclient.ts +++ b/packages/gsul/src/dynamoDBclient.ts @@ -1,29 +1,18 @@ +import {DynamoDBClient} from "@aws-sdk/client-dynamodb" import {DynamoDBDocumentClient, QueryCommand, QueryCommandInput} from "@aws-sdk/lib-dynamodb" import {Logger} from "@aws-lambda-powertools/logger" -import {DynamoDBResult} from "./schema/result.ts" +import {itemType} from "./schema/response.ts" import {NativeAttributeValue} from "@aws-sdk/util-dynamodb" -import {inputPrescriptionType} from "./schema/request.ts" const tableName = process.env.TABLE_NAME +const client = new DynamoDBClient() +const docClient = DynamoDBDocumentClient.from(client) -export function createDynamoDBQuery(prescription: inputPrescriptionType) { - const queryParam: QueryCommandInput = { - TableName: tableName, - IndexName: "PharmacyODSCodePrescriptionIDIndex", - KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", - ExpressionAttributeValues: { - ":inputPharmacyODSCode": prescription.odsCode, - ":inputPrescriptionID": prescription.prescriptionID - } - } - return queryParam -} - -export async function runDynamoDBQuery( - query: QueryCommandInput, - docClient: DynamoDBDocumentClient, +export async function getItemsUpdatesForPrescription( + prescriptionID: string, + odsCode: string, logger: Logger -): Promise> { +): Promise> { // helper function to deal with pagination of results from dynamodb const getAllData = async (query: QueryCommandInput) => { const _getAllData = async (query: QueryCommandInput, startKey: Record) => { @@ -44,22 +33,20 @@ export async function runDynamoDBQuery( return rows } - const items = await getAllData(query) - - if (items.length !== 0) { - const response: Array = items.map((singleUpdate) => { - const result: DynamoDBResult = { - prescriptionID: String(singleUpdate.PrescriptionID), - itemId: String(singleUpdate.LineItemID), - latestStatus: String(singleUpdate.Status), - isTerminalState: String(singleUpdate.TerminalStatus), - lastUpdateDateTime: String(singleUpdate.LastModified) - } - return result - }) - return response - } - const result: Array = [] + const items = await getAllData({ + TableName: tableName, + IndexName: "PharmacyODSCodePrescriptionIDIndex", + KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", + ExpressionAttributeValues: { + ":inputPharmacyODSCode": odsCode, + ":inputPrescriptionID": prescriptionID + } + }) - return result + return items.map((singleUpdate) => ({ + itemId: String(singleUpdate.LineItemID), + latestStatus: String(singleUpdate.Status), + isTerminalState: String(singleUpdate.TerminalStatus), + lastUpdateDateTime: String(singleUpdate.LastModified) + })) } diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index 8ff474f40..9e2b28a3d 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -1,21 +1,16 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import {Logger} from "@aws-lambda-powertools/logger" import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" -import {DynamoDBClient} from "@aws-sdk/client-dynamodb" -import {DynamoDBDocumentClient} from "@aws-sdk/lib-dynamodb" import middy from "@middy/core" import inputOutputLogger from "@middy/input-output-logger" import validator from "@middy/validator" import {transpileSchema} from "@middy/validator/transpile" import {errorHandler} from "./errorHandler.ts" -import {createDynamoDBQuery, runDynamoDBQuery} from "./dynamoDBclient.ts" +import {getItemsUpdatesForPrescription} from "./dynamoDBclient.ts" import {requestSchema, requestType, inputPrescriptionType} from "./schema/request.ts" import {responseType, outputPrescriptionType, itemType} from "./schema/response.ts" -import {DynamoDBResult} from "./schema/result.ts" const logger = new Logger({serviceName: "GSUL"}) -const client = new DynamoDBClient({region: "eu-west-2"}) -const docClient = DynamoDBDocumentClient.from(client) const lambdaHandler = async (event: requestType): Promise => { // there are deliberately no try..catch blocks in this as any errors are caught by custom middy error handler @@ -23,10 +18,8 @@ const lambdaHandler = async (event: requestType): Promise => { // this is an async map so it returns an array of promises const itemResults = event.prescriptions.map(async (prescription) => { - const query = createDynamoDBQuery(prescription) - const queryResult = await runDynamoDBQuery(query, docClient, logger) - const result = buildResult(prescription, queryResult) - return result + const queryResult = await getItemsUpdatesForPrescription(prescription.prescriptionID, prescription.odsCode, logger) + return buildResult(prescription, queryResult) }) // wait for all the promises to complete @@ -41,56 +34,28 @@ const lambdaHandler = async (event: requestType): Promise => { export const buildResult = ( inputPrescription: inputPrescriptionType, - items: Array + items: Array ): outputPrescriptionType => { - // if we have results then populate the response - if (items.length > 0) { - // we need to get the latest update per item id - // first get the unique item ids - const uniqueItemIds = [ - ...new Set( - items.map((item) => { - return item.itemId - }) - ) - ] + // store an empty map of itemId to latest update + const uniqueItemMap: Map = new Map() - // now get an array of the latest updates for each unique item id - const latestUpdates = uniqueItemIds.map((itemId) => { - // get all the updates for this item id - const matchedItems = items.filter((item) => { - return item.itemId === itemId - }) - - // now just get the latest update - const latestUpdate = matchedItems.reduce((prev, current) => { - return prev && Date.parse(prev.lastUpdateDateTime) > Date.parse(current.lastUpdateDateTime) ? prev : current - }) - - return latestUpdate - }) - - const responseItems = latestUpdates.map((item) => { - const returnItem: itemType = { - itemId: String(item.itemId), - latestStatus: String(item.latestStatus), - isTerminalState: String(item.isTerminalState), - lastUpdateDateTime: String(item.lastUpdateDateTime) + for (const item of items) { + // if the itemId hasn't been seen store the update + if (!uniqueItemMap.has(item.itemId)) { + uniqueItemMap.set(item.itemId, item) + } else { + // if the existing update is older overwrite it + const prev = uniqueItemMap.get(item.itemId) + if (Date.parse(prev.lastUpdateDateTime) < Date.parse(item.lastUpdateDateTime)) { + uniqueItemMap.set(item.itemId, item) } - return returnItem - }) - const result: outputPrescriptionType = { - prescriptionID: inputPrescription.prescriptionID, - onboarded: true, - items: responseItems } - return result } - // we have no results returned so return an empty array of items - const result = { + + const result: outputPrescriptionType = { prescriptionID: inputPrescription.prescriptionID, - onboarded: false, - items: [] + onboarded: items.length > 0, + items: [...uniqueItemMap.values()] } return result } diff --git a/packages/gsul/src/schema/result.ts b/packages/gsul/src/schema/result.ts deleted file mode 100644 index cd195e084..000000000 --- a/packages/gsul/src/schema/result.ts +++ /dev/null @@ -1,9 +0,0 @@ -interface DynamoDBResult { - prescriptionID: string | undefined; - itemId: string | undefined; - latestStatus: string | undefined; - isTerminalState: string | undefined; - lastUpdateDateTime: string | undefined - } - -export {DynamoDBResult} diff --git a/packages/gsul/tests/testBuildResult.test.ts b/packages/gsul/tests/testBuildResult.test.ts index c8f5ba04d..d0ff359c5 100644 --- a/packages/gsul/tests/testBuildResult.test.ts +++ b/packages/gsul/tests/testBuildResult.test.ts @@ -1,12 +1,11 @@ import {buildResult} from "../src/getStatusUpdates" import {inputPrescriptionType} from "../src/schema/request" -import {DynamoDBResult} from "../src/schema/result" -import {outputPrescriptionType} from "../src/schema/response" +import {outputPrescriptionType, itemType} from "../src/schema/response" type scenariosType = { scenarioDescription: string inputPrescriptions: inputPrescriptionType - queryResults: Array + queryResults: Array expectedResult: outputPrescriptionType } const scenarios: Array = [ @@ -18,7 +17,6 @@ const scenarios: Array = [ }, queryResults: [ { - prescriptionID: "abc", itemId: "item_1", latestStatus: "latest_status", isTerminalState: "is_terminal_status", @@ -59,14 +57,12 @@ const scenarios: Array = [ }, queryResults: [ { - prescriptionID: "abc", itemId: "item_1", latestStatus: "early_status", isTerminalState: "is_terminal_status", lastUpdateDateTime: "1970-01-01T00:00:00Z" }, { - prescriptionID: "abc", itemId: "item_1", latestStatus: "latest_status", isTerminalState: "is_terminal_status", @@ -94,28 +90,24 @@ const scenarios: Array = [ }, queryResults: [ { - prescriptionID: "abc", itemId: "item_1", latestStatus: "item_1_status", isTerminalState: "is_terminal_status", lastUpdateDateTime: "1970-01-01T00:00:00Z" }, { - prescriptionID: "abc", itemId: "item_1", latestStatus: "latest_item_1_status", isTerminalState: "is_terminal_status", lastUpdateDateTime: "1972-01-01T00:00:00Z" }, { - prescriptionID: "abc", itemId: "item_2", latestStatus: "item_2_status", isTerminalState: "is_terminal_status", lastUpdateDateTime: "1971-01-01T00:00:00Z" }, { - prescriptionID: "abc", itemId: "item_2", latestStatus: "early_item_2_status", isTerminalState: "is_terminal_status", diff --git a/packages/gsul/tests/testRunDynamoDBQueries.test.ts b/packages/gsul/tests/testRunDynamoDBQueries.test.ts index 380bfca51..2cd04979d 100644 --- a/packages/gsul/tests/testRunDynamoDBQueries.test.ts +++ b/packages/gsul/tests/testRunDynamoDBQueries.test.ts @@ -1,6 +1,5 @@ -import {QueryCommandInput, DynamoDBDocumentClient} from "@aws-sdk/lib-dynamodb" -import {DynamoDBClient} from "@aws-sdk/client-dynamodb" -import {runDynamoDBQuery} from "../src/dynamoDBclient" +import {DynamoDBDocumentClient} from "@aws-sdk/lib-dynamodb" +import {getItemsUpdatesForPrescription} from "../src/dynamoDBclient" import {Logger} from "@aws-lambda-powertools/logger" import { expect, @@ -11,9 +10,6 @@ import { const logger = new Logger({serviceName: "GSUL_TEST"}) -const client = new DynamoDBClient({region: "eu-west-2"}) -const docClient = DynamoDBDocumentClient.from(client) - describe("testing dynamoDBClient", () => { beforeEach(() => { jest.resetModules() @@ -34,22 +30,11 @@ describe("testing dynamoDBClient", () => { }) it("should call dynamo once and return expected items", async () => { - const queryParams: QueryCommandInput = { - TableName: "tableName", - IndexName: "indexName", - KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", - ExpressionAttributeValues: { - ":inputPharmacyODSCode": "odsCode", - ":inputPrescriptionID": "prescriptionID" - } - } - - const queryResults = await runDynamoDBQuery(queryParams, docClient, logger) + const queryResults = await getItemsUpdatesForPrescription("prescriptionID", "odsCode", logger) expect(DynamoDBDocumentClient.prototype.send).toHaveBeenCalledTimes(1) expect(queryResults).toMatchObject([ { - prescriptionID: "abc", itemId: "item_1", latestStatus: "latest_status", isTerminalState: "is_terminal_status", @@ -99,27 +84,16 @@ describe("testing pagination in dynamoDBClient", () => { }) it("should handle pagination", async () => { - const queryParams: QueryCommandInput = { - TableName: "tableName", - IndexName: "indexName", - KeyConditionExpression: "PrescriptionID = :inputPrescriptionID AND PharmacyODSCode = :inputPharmacyODSCode", - ExpressionAttributeValues: { - ":inputPharmacyODSCode": "odsCode", - ":inputPrescriptionID": "prescriptionID" - } - } - const queryResults = await runDynamoDBQuery(queryParams, docClient, logger) + const queryResults = await getItemsUpdatesForPrescription("prescriptionID", "odsCode", logger) expect(queryResults).toMatchObject([ { - prescriptionID: "abc", itemId: "item_1", latestStatus: "first_status", isTerminalState: "is_terminal_status", lastUpdateDateTime: "1970-01-01T00:00:00Z" }, { - prescriptionID: "abc", itemId: "item_1", latestStatus: "second_status", isTerminalState: "is_terminal_status", From 19e39753d62397c13142be909c0f43426d733153 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:36:34 +0000 Subject: [PATCH 30/33] avoid for loop --- packages/gsul/src/getStatusUpdates.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index 9e2b28a3d..2839f23e9 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -36,26 +36,19 @@ export const buildResult = ( inputPrescription: inputPrescriptionType, items: Array ): outputPrescriptionType => { - // store an empty map of itemId to latest update - const uniqueItemMap: Map = new Map() - - for (const item of items) { - // if the itemId hasn't been seen store the update - if (!uniqueItemMap.has(item.itemId)) { - uniqueItemMap.set(item.itemId, item) - } else { - // if the existing update is older overwrite it - const prev = uniqueItemMap.get(item.itemId) - if (Date.parse(prev.lastUpdateDateTime) < Date.parse(item.lastUpdateDateTime)) { - uniqueItemMap.set(item.itemId, item) - } - } - } + // get unique item ids with the latest update based on lastUpdateDateTime + const uniqueItems: Array = Object.values( + items.reduce(function (r, e) { + if (!r[e.itemId]) r[e.itemId] = e + else if (e.lastUpdateDateTime > r[e.itemId].lastUpdateDateTime) r[e.itemId] = e + return r + }, {}) + ) const result: outputPrescriptionType = { prescriptionID: inputPrescription.prescriptionID, onboarded: items.length > 0, - items: [...uniqueItemMap.values()] + items: uniqueItems } return result } From eda9deade0e8bb8365d6211f74f3bf41c83e77ad Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:44:55 +0000 Subject: [PATCH 31/33] parse date --- packages/gsul/src/getStatusUpdates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index 2839f23e9..80588e642 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -40,7 +40,7 @@ export const buildResult = ( const uniqueItems: Array = Object.values( items.reduce(function (r, e) { if (!r[e.itemId]) r[e.itemId] = e - else if (e.lastUpdateDateTime > r[e.itemId].lastUpdateDateTime) r[e.itemId] = e + else if (Date.parse(e.lastUpdateDateTime) > Date.parse(r[e.itemId].lastUpdateDateTime)) r[e.itemId] = e return r }, {}) ) From 1241bfcfdabbd00b2e8f9bae804a8b3f7a898c23 Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:34:46 +0000 Subject: [PATCH 32/33] correct logging --- .../updatePrescriptionStatus/src/updatePrescriptionStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts b/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts index 46b23e37f..adf6e27c4 100644 --- a/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts +++ b/packages/updatePrescriptionStatus/src/updatePrescriptionStatus.ts @@ -36,7 +36,7 @@ const lambdaHandler = async (event: APIGatewayProxyEvent): Promise = [] From f0febcdeca51aa67a647c74585073d936f51516e Mon Sep 17 00:00:00 2001 From: Anthony Brown <121869075+anthony-nhs@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:47:05 +0000 Subject: [PATCH 33/33] simplify things even further --- packages/gsul/src/getStatusUpdates.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/gsul/src/getStatusUpdates.ts b/packages/gsul/src/getStatusUpdates.ts index 80588e642..4dbba031d 100644 --- a/packages/gsul/src/getStatusUpdates.ts +++ b/packages/gsul/src/getStatusUpdates.ts @@ -39,8 +39,7 @@ export const buildResult = ( // get unique item ids with the latest update based on lastUpdateDateTime const uniqueItems: Array = Object.values( items.reduce(function (r, e) { - if (!r[e.itemId]) r[e.itemId] = e - else if (Date.parse(e.lastUpdateDateTime) > Date.parse(r[e.itemId].lastUpdateDateTime)) r[e.itemId] = e + if (!r[e.itemId] || Date.parse(e.lastUpdateDateTime) > Date.parse(r[e.itemId].lastUpdateDateTime)) r[e.itemId] = e return r }, {}) )