From 0775198ede19fec645fdd6c98aeafdbd48d5cd0d Mon Sep 17 00:00:00 2001 From: Anthony Brown Date: Tue, 10 Feb 2026 10:17:00 +0000 Subject: [PATCH 01/29] add process lambda --- package.json | 3 +- packages/cdk/resources/Functions.ts | 13 +++++++ packages/process/package.json | 28 +++++++++++++++ packages/process/src/handler.ts | 53 +++++++++++++++++++++++++++++ packages/process/tsconfig.json | 9 +++++ 5 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 packages/process/package.json create mode 100644 packages/process/src/handler.ts create mode 100644 packages/process/tsconfig.json diff --git a/package.json b/package.json index 544ae6b..4f6e184 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "workspaces": [ "packages/hack", "packages/cdk", - "packages/foo" + "packages/foo", + "packages/process" ], "devDependencies": { "@eslint/js": "^9.38.0", diff --git a/packages/cdk/resources/Functions.ts b/packages/cdk/resources/Functions.ts index 3c22a45..3d90083 100644 --- a/packages/cdk/resources/Functions.ts +++ b/packages/cdk/resources/Functions.ts @@ -15,6 +15,7 @@ export interface ApiFunctionsProps { */ export class ApiFunctions extends Construct { public readonly fooLambda: TypescriptLambdaFunction + public readonly processLambda: TypescriptLambdaFunction public constructor(scope: Construct, id: string, props: ApiFunctionsProps) { super(scope, id) @@ -29,7 +30,19 @@ export class ApiFunctions extends Construct { version: props.version, commitId: props.commitId }) + const processLambda = new TypescriptLambdaFunction(this, "ProcessLambda", { + functionName: `${props.stackName}-ProcessLambda`, + projectBaseDir: baseDir, + packageBasePath: "packages/process", + entryPoint: "src/handler.ts", + environmentVariables: {}, + logRetentionInDays: 30, + logLevel: "DEBUG", + version: props.version, + commitId: props.commitId + }) // Outputs this.fooLambda = fooLambda + this.processLambda = processLambda } } diff --git a/packages/process/package.json b/packages/process/package.json new file mode 100644 index 0000000..b711ae3 --- /dev/null +++ b/packages/process/package.json @@ -0,0 +1,28 @@ +{ + "name": "process", + "version": "1.0.0", + "description": "Lambda of the procss endpoint", + "main": "status.js", + "author": "NHS Digital", + "license": "MIT", + "scripts": { + "unit": "POWERTOOLS_DEV=true NODE_OPTIONS=--experimental-vm-modules jest --no-cache --coverage", + "lint": "eslint --max-warnings 0 --fix --config ../../eslint.config.mjs .", + "compile": "tsc --build", + "test": "npm run compile && npm run unit", + "check-licenses": "license-checker --failOn GPL --failOn LGPL --start ../.." + }, + "dependencies": { + "@aws-lambda-powertools/commons": "^2.30.2", + "@aws-lambda-powertools/logger": "^2.30.2", + "@aws-lambda-powertools/parameters": "^2.30.2", + "@middy/core": "^7.0.2", + "@middy/input-output-logger": "^7.0.2", + "@nhs/fhir-middy-error-handler": "^2.1.71", + "@nhsdigital/eps-spine-client": "^2.1.78" + }, + "devDependencies": { + "axios-mock-adapter": "^2.1.0", + "esbuild": "^0.27.2" + } +} \ No newline at end of file diff --git a/packages/process/src/handler.ts b/packages/process/src/handler.ts new file mode 100644 index 0000000..d3996bd --- /dev/null +++ b/packages/process/src/handler.ts @@ -0,0 +1,53 @@ +import {Logger} from "@aws-lambda-powertools/logger" +import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" +import middy from "@middy/core" +import inputOutputLogger from "@middy/input-output-logger" +import errorHandler from "@nhs/fhir-middy-error-handler" + +const logger = new Logger({serviceName: "status"}) + +/** + * + * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + * @param {Object} _event - API Gateway Lambda Proxy Input Format + * + * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + * @returns {Object} object - API Gateway Lambda Proxy Output Format + * + */ + +const lambdaHandler = async (event: any): Promise => { + logger.appendKeys({ + "nhsd-correlation-id": event.headers["nhsd-correlation-id"], + "x-request-id": event.headers["x-request-id"], + "nhsd-request-id": event.headers["nhsd-request-id"], + "x-correlation-id": event.headers["x-correlation-id"], + "apigw-request-id": event.requestContext.requestId + }) + + const commitId = process.env.COMMIT_ID + const versionNumber = process.env.VERSION_NUMBER + + + const statusBody = {commitId: commitId, versionNumber: versionNumber} + + return { + statusCode: 200, + body: JSON.stringify(statusBody), + headers: { + "Content-Type": "application/health+json", + "Cache-Control": "no-cache" + } + } +} + +export const handler = middy(lambdaHandler) + .use(injectLambdaContext(logger, {clearState: true})) + .use( + inputOutputLogger({ + logger: (request) => { + logger.info(request) + } + }) + ) + .use(errorHandler({logger: logger})) \ No newline at end of file diff --git a/packages/process/tsconfig.json b/packages/process/tsconfig.json new file mode 100644 index 0000000..d26c8f0 --- /dev/null +++ b/packages/process/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.defaults.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "lib" + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file From 680cf66929429be8f34f3e25d314d9628a0455fe Mon Sep 17 00:00:00 2001 From: Anthony Brown Date: Tue, 10 Feb 2026 10:18:40 +0000 Subject: [PATCH 02/29] update package.lock --- package-lock.json | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6766051..0af633c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "workspaces": [ "packages/hack", "packages/cdk", - "packages/foo" + "packages/foo", + "packages/process" ], "dependencies": { "conventional-changelog-eslint": "^6.0.0", @@ -3344,7 +3345,6 @@ "resolved": "https://registry.npmjs.org/@middy/core/-/core-7.0.2.tgz", "integrity": "sha512-C4PEJxMjWFneqfQzsbWQ9BZ7Bfds9oqw74/fTuEeunI/0PA1KtGfbHDuU1SQH6lZ3rOp+VUIYh4YDho8qmc2Rg==", "license": "MIT", - "peer": true, "engines": { "node": ">=22" }, @@ -12332,6 +12332,10 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "resolved": "packages/process", + "link": true + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -15523,6 +15527,23 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" + }, + "packages/process": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@aws-lambda-powertools/commons": "^2.30.2", + "@aws-lambda-powertools/logger": "^2.30.2", + "@aws-lambda-powertools/parameters": "^2.30.2", + "@middy/core": "^7.0.2", + "@middy/input-output-logger": "^7.0.2", + "@nhs/fhir-middy-error-handler": "^2.1.71", + "@nhsdigital/eps-spine-client": "^2.1.78" + }, + "devDependencies": { + "axios-mock-adapter": "^2.1.0", + "esbuild": "^0.27.2" + } } } } From fa81b15def896bd39f8692454229cbf01349defd Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 10:19:52 +0000 Subject: [PATCH 03/29] Add an empty prescription creation lambda --- packages/cdk/resources/Functions.ts | 18 ++++++- .../RestApiGateway/RestApiGatewayMethods.ts | 19 ++++--- packages/cdk/stacks/HackStack.ts | 31 +++++------ packages/create/package.json | 28 ++++++++++ packages/create/src/handler.ts | 53 +++++++++++++++++++ packages/create/tsconfig.json | 9 ++++ 6 files changed, 136 insertions(+), 22 deletions(-) create mode 100644 packages/create/package.json create mode 100644 packages/create/src/handler.ts create mode 100644 packages/create/tsconfig.json diff --git a/packages/cdk/resources/Functions.ts b/packages/cdk/resources/Functions.ts index 3c22a45..ab0a941 100644 --- a/packages/cdk/resources/Functions.ts +++ b/packages/cdk/resources/Functions.ts @@ -1,6 +1,6 @@ import {Construct} from "constructs" import {TypescriptLambdaFunction} from "@nhsdigital/eps-cdk-constructs" -import { resolve } from "node:path" +import {resolve} from "node:path" const baseDir = resolve(__dirname, "../../..") // Interface for properties needed to create API functions export interface ApiFunctionsProps { @@ -15,6 +15,7 @@ export interface ApiFunctionsProps { */ export class ApiFunctions extends Construct { public readonly fooLambda: TypescriptLambdaFunction + public readonly createLambda: TypescriptLambdaFunction public constructor(scope: Construct, id: string, props: ApiFunctionsProps) { super(scope, id) @@ -29,7 +30,22 @@ export class ApiFunctions extends Construct { version: props.version, commitId: props.commitId }) + + const createLambda = new TypescriptLambdaFunction(this, "CreateLambda", { + functionName: `${props.stackName}-CreateLambda`, + projectBaseDir: baseDir, + packageBasePath: "packages/create", + entryPoint: "src/handler.ts", + environmentVariables: {}, + logRetentionInDays: 30, + logLevel: "DEBUG", + version: props.version, + commitId: props.commitId + }) + + // Outputs this.fooLambda = fooLambda + this.createLambda = createLambda } } diff --git a/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts b/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts index fb6e8fa..3d17022 100644 --- a/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts +++ b/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts @@ -5,7 +5,7 @@ import { RestApi } from "aws-cdk-lib/aws-apigateway" import {Construct} from "constructs" -import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs" +import {NodejsFunction} from "aws-cdk-lib/aws-lambda-nodejs" // import {NodejsFunction} from "aws-cdk-lib/aws-lambda-nodejs" export interface RestApiGatewayMethodsProps { @@ -13,6 +13,7 @@ export interface RestApiGatewayMethodsProps { readonly restAPiGatewayRole: IRole readonly restApiGateway: RestApi readonly fooLambda: NodejsFunction + readonly createLambda: NodejsFunction } /** @@ -35,11 +36,17 @@ export class RestApiGatewayMethods extends Construct { const prescriptionDetailsLambdaResource = props.restApiGateway.root.addResource("foo") prescriptionDetailsLambdaResource.addMethod("GET", new LambdaIntegration(props.fooLambda, { - credentialsRole: props.restAPiGatewayRole - }), { - authorizationType: AuthorizationType.NONE, - }) - + credentialsRole: props.restAPiGatewayRole + }), { + authorizationType: AuthorizationType.NONE, + }) + + const createPrescriptionLambdaResource = props.restApiGateway.root.addResource("create") + createPrescriptionLambdaResource.addMethod("POST", new LambdaIntegration(props.createLambda, { + credentialsRole: props.restAPiGatewayRole + }), { + authorizationType: AuthorizationType.NONE, + }) //Outputs } diff --git a/packages/cdk/stacks/HackStack.ts b/packages/cdk/stacks/HackStack.ts index 2d9174e..ec0e9db 100644 --- a/packages/cdk/stacks/HackStack.ts +++ b/packages/cdk/stacks/HackStack.ts @@ -11,18 +11,18 @@ import {StaticContentBucket} from "../resources/StaticContentBucket" import {Certificate} from "aws-cdk-lib/aws-certificatemanager" import {Role} from "aws-cdk-lib/aws-iam" import {HostedZone} from "aws-cdk-lib/aws-route53" -import { Key } from "aws-cdk-lib/aws-kms" -import { Stream } from "aws-cdk-lib/aws-kinesis" -import { ukRegionLogGroups } from "../resources/ukRegionLogGroups" -import { RestApiGateway } from "../resources/RestApiGateway" -import { RestApiGatewayMethods } from "../resources/RestApiGateway/RestApiGatewayMethods" -import { RestApiOrigin, S3BucketOrigin } from "aws-cdk-lib/aws-cloudfront-origins" -import { AccessLevel, AllowedMethods, FunctionEventType, OriginRequestCookieBehavior, OriginRequestHeaderBehavior, OriginRequestPolicy, OriginRequestQueryStringBehavior, ViewerProtocolPolicy } from "aws-cdk-lib/aws-cloudfront" -import { CloudfrontBehaviors } from "../resources/CloudfrontBehaviors" -import { CloudfrontDistribution } from "../resources/CloudfrontDistribution" -import { getConfigFromEnvVar } from "@nhsdigital/eps-cdk-constructs" -import { ApiFunctions } from "../resources/Functions" -import { addNagSuppressions } from "./nagSuppression" +import {Key} from "aws-cdk-lib/aws-kms" +import {Stream} from "aws-cdk-lib/aws-kinesis" +import {ukRegionLogGroups} from "../resources/ukRegionLogGroups" +import {RestApiGateway} from "../resources/RestApiGateway" +import {RestApiGatewayMethods} from "../resources/RestApiGateway/RestApiGatewayMethods" +import {RestApiOrigin, S3BucketOrigin} from "aws-cdk-lib/aws-cloudfront-origins" +import {AccessLevel, AllowedMethods, FunctionEventType, OriginRequestCookieBehavior, OriginRequestHeaderBehavior, OriginRequestPolicy, OriginRequestQueryStringBehavior, ViewerProtocolPolicy} from "aws-cdk-lib/aws-cloudfront" +import {CloudfrontBehaviors} from "../resources/CloudfrontBehaviors" +import {CloudfrontDistribution} from "../resources/CloudfrontDistribution" +import {getConfigFromEnvVar} from "@nhsdigital/eps-cdk-constructs" +import {ApiFunctions} from "../resources/Functions" +import {addNagSuppressions} from "./nagSuppression" export interface HackStackProps extends StackProps { readonly serviceName: string @@ -37,7 +37,7 @@ export interface HackStackProps extends StackProps { */ export class HackStack extends Stack { - public constructor(scope: App, id: string, props: HackStackProps){ + public constructor(scope: App, id: string, props: HackStackProps) { super(scope, id, props) // Context @@ -114,7 +114,8 @@ export class HackStack extends Stack { ], restAPiGatewayRole: apiGateway.apiGatewayRole, restApiGateway: apiGateway.apiGateway, - fooLambda: functions.fooLambda.function + fooLambda: functions.fooLambda.function, + createLambda: functions.createLambda.function }) const staticContentBucketOrigin = S3BucketOrigin.withOriginAccessControl( staticContentBucket.bucket, @@ -141,7 +142,7 @@ export class HackStack extends Stack { apiGatewayRequestPolicy: apiGatewayRequestPolicy, staticContentBucketOrigin: staticContentBucketOrigin, }) - + // --- Distribution const cloudfrontDistribution = new CloudfrontDistribution(this, "CloudfrontDistribution", { serviceName: props.serviceName, diff --git a/packages/create/package.json b/packages/create/package.json new file mode 100644 index 0000000..d78bcbc --- /dev/null +++ b/packages/create/package.json @@ -0,0 +1,28 @@ +{ + "name": "status", + "version": "1.0.0", + "description": "Lambda of the _status endpoint", + "main": "status.js", + "author": "NHS Digital", + "license": "MIT", + "scripts": { + "unit": "POWERTOOLS_DEV=true NODE_OPTIONS=--experimental-vm-modules jest --no-cache --coverage", + "lint": "eslint --max-warnings 0 --fix --config ../../eslint.config.mjs .", + "compile": "tsc --build", + "test": "npm run compile && npm run unit", + "check-licenses": "license-checker --failOn GPL --failOn LGPL --start ../.." + }, + "dependencies": { + "@aws-lambda-powertools/commons": "^2.30.2", + "@aws-lambda-powertools/logger": "^2.30.2", + "@aws-lambda-powertools/parameters": "^2.30.2", + "@middy/core": "^7.0.2", + "@middy/input-output-logger": "^7.0.2", + "@nhs/fhir-middy-error-handler": "^2.1.71", + "@nhsdigital/eps-spine-client": "^2.1.78" + }, + "devDependencies": { + "axios-mock-adapter": "^2.1.0", + "esbuild": "^0.27.2" + } +} \ No newline at end of file diff --git a/packages/create/src/handler.ts b/packages/create/src/handler.ts new file mode 100644 index 0000000..d3996bd --- /dev/null +++ b/packages/create/src/handler.ts @@ -0,0 +1,53 @@ +import {Logger} from "@aws-lambda-powertools/logger" +import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" +import middy from "@middy/core" +import inputOutputLogger from "@middy/input-output-logger" +import errorHandler from "@nhs/fhir-middy-error-handler" + +const logger = new Logger({serviceName: "status"}) + +/** + * + * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + * @param {Object} _event - API Gateway Lambda Proxy Input Format + * + * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + * @returns {Object} object - API Gateway Lambda Proxy Output Format + * + */ + +const lambdaHandler = async (event: any): Promise => { + logger.appendKeys({ + "nhsd-correlation-id": event.headers["nhsd-correlation-id"], + "x-request-id": event.headers["x-request-id"], + "nhsd-request-id": event.headers["nhsd-request-id"], + "x-correlation-id": event.headers["x-correlation-id"], + "apigw-request-id": event.requestContext.requestId + }) + + const commitId = process.env.COMMIT_ID + const versionNumber = process.env.VERSION_NUMBER + + + const statusBody = {commitId: commitId, versionNumber: versionNumber} + + return { + statusCode: 200, + body: JSON.stringify(statusBody), + headers: { + "Content-Type": "application/health+json", + "Cache-Control": "no-cache" + } + } +} + +export const handler = middy(lambdaHandler) + .use(injectLambdaContext(logger, {clearState: true})) + .use( + inputOutputLogger({ + logger: (request) => { + logger.info(request) + } + }) + ) + .use(errorHandler({logger: logger})) \ No newline at end of file diff --git a/packages/create/tsconfig.json b/packages/create/tsconfig.json new file mode 100644 index 0000000..d26c8f0 --- /dev/null +++ b/packages/create/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.defaults.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "lib" + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file From 3470e945561827dfdbe7217d748df7dc185ed842 Mon Sep 17 00:00:00 2001 From: Anthony Brown Date: Tue, 10 Feb 2026 10:23:15 +0000 Subject: [PATCH 04/29] add uuid --- packages/process/src/handler.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/process/src/handler.ts b/packages/process/src/handler.ts index d3996bd..1cf0dec 100644 --- a/packages/process/src/handler.ts +++ b/packages/process/src/handler.ts @@ -3,23 +3,29 @@ import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" import middy from "@middy/core" import inputOutputLogger from "@middy/input-output-logger" import errorHandler from "@nhs/fhir-middy-error-handler" +import crypto from "crypto" -const logger = new Logger({serviceName: "status"}) +const logger = new Logger({serviceName: "process"}) + +const generateUuid = (): string => { + if (typeof crypto.randomUUID === "function") { + return crypto.randomUUID() + } + + const bytes = crypto.randomBytes(16) + bytes[6] = (bytes[6] & 0x0f) | 0x40 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + const hex = bytes.toString("hex") + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` +} -/** - * - * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format - * @param {Object} _event - API Gateway Lambda Proxy Input Format - * - * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html - * @returns {Object} object - API Gateway Lambda Proxy Output Format - * - */ const lambdaHandler = async (event: any): Promise => { + const requestId = event.headers["x-request-id"] ?? generateUuid() + logger.appendKeys({ "nhsd-correlation-id": event.headers["nhsd-correlation-id"], - "x-request-id": event.headers["x-request-id"], + "x-request-id": requestId, "nhsd-request-id": event.headers["nhsd-request-id"], "x-correlation-id": event.headers["x-correlation-id"], "apigw-request-id": event.requestContext.requestId From d6b700043cb2dee7cbbadb501c4db9534990976f Mon Sep 17 00:00:00 2001 From: Anthony Brown Date: Tue, 10 Feb 2026 10:40:55 +0000 Subject: [PATCH 05/29] add dynamo --- packages/cdk/resources/DynamoDb.ts | 141 +++++++++++++++++++++++++++++ packages/cdk/stacks/HackStack.ts | 5 + 2 files changed, 146 insertions(+) create mode 100644 packages/cdk/resources/DynamoDb.ts diff --git a/packages/cdk/resources/DynamoDb.ts b/packages/cdk/resources/DynamoDb.ts new file mode 100644 index 0000000..4958105 --- /dev/null +++ b/packages/cdk/resources/DynamoDb.ts @@ -0,0 +1,141 @@ +import {Construct} from "constructs" + +import { + AttributeType, + Billing, + TableEncryptionV2, + TableV2 +} from "aws-cdk-lib/aws-dynamodb" +import { + AccountRootPrincipal, + AnyPrincipal, + Effect, + ManagedPolicy, + PolicyDocument, + PolicyStatement +} from "aws-cdk-lib/aws-iam" +import {Key} from "aws-cdk-lib/aws-kms" +import {Duration, RemovalPolicy} from "aws-cdk-lib" + +export interface DynamodbProps { + readonly stackName: string +} + +/** + * Dynamodb tables used for user state information + */ +export class Dynamodb extends Construct { + public readonly processStatus: TableV2 + public readonly useProcessStatusKmsKeyPolicy: ManagedPolicy + public readonly processStatusTableWritePolicy: ManagedPolicy + public readonly processStatusTableReadPolicy: ManagedPolicy + // + + public constructor(scope: Construct, id: string, props: DynamodbProps) { + super(scope, id) + + // KMS key for token mapping table + const processStatusKey = new Key(this, "processStatusKey", { + removalPolicy: RemovalPolicy.DESTROY, + pendingWindow: Duration.days(7), + alias: `${props.stackName}-processStatusKey`, + description: `${props.stackName}-processStatusKey`, + enableKeyRotation: true, + policy: new PolicyDocument({ + statements: [ + new PolicyStatement({ + sid: "Enable IAM User Permissions", + effect: Effect.ALLOW, + actions: [ + "kms:*" + ], + principals: [ + new AccountRootPrincipal + ], + resources: ["*"] + }) + ] + }) + }) + + // Process Status Table + const processStatusTable = new TableV2(this, "processStatusTable", { + partitionKey: { + name: "actionId", + type: AttributeType.STRING + }, + tableName: `${props.stackName}-processStatus`, + removalPolicy: RemovalPolicy.DESTROY, + pointInTimeRecoverySpecification: { + pointInTimeRecoveryEnabled: true + }, + encryption: TableEncryptionV2.customerManagedKey(processStatusKey), + billing: Billing.onDemand(), + timeToLiveAttribute: "ExpiryTime" + }) + + // Policy to use token mapping KMS key + const useProcessStatusKmsKey = new ManagedPolicy(this, "UseProcessStatusKMSKeyPolicy", { + statements: [ + new PolicyStatement({ + actions: [ + "kms:DescribeKey", + "kms:GenerateDataKey", + "kms:Encrypt", + "kms:ReEncryptFrom", + "kms:ReEncryptTo", + "kms:Decrypt" + ], + resources: [ + processStatusKey.keyArn + ] + }) + ] + }) + + const processStatusReadPolicy = new ManagedPolicy(this, "ProcessStatusReadManagedPolicy", { + statements: [ + new PolicyStatement({ + actions: [ + "dynamodb:GetItem", + "dynamodb:BatchGetItem", + "dynamodb:Scan", + "dynamodb:Query", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable" + ], + resources: [ + processStatusTable.tableArn, + `${processStatusTable.tableArn}/index/*` + ] + }) + ] + }) + + const processStatusWritePolicy = new ManagedPolicy(this, "ProcessStatusWriteManagedPolicy", { + statements: [ + new PolicyStatement({ + actions: [ + "dynamodb:PutItem", + "dynamodb:BatchWriteItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem" + ], + resources: [ + processStatusTable.tableArn, + `${processStatusTable.tableArn}/index/*` + ] + }) + ] + }) + + + // Outputs: assign the created resources to the class properties + this.processStatus = processStatusTable + this.useProcessStatusKmsKeyPolicy = useProcessStatusKmsKey + this.processStatusTableWritePolicy = processStatusWritePolicy + this.processStatusTableReadPolicy = processStatusReadPolicy + + + } +} diff --git a/packages/cdk/stacks/HackStack.ts b/packages/cdk/stacks/HackStack.ts index 2d9174e..5cc2da1 100644 --- a/packages/cdk/stacks/HackStack.ts +++ b/packages/cdk/stacks/HackStack.ts @@ -23,6 +23,7 @@ import { CloudfrontDistribution } from "../resources/CloudfrontDistribution" import { getConfigFromEnvVar } from "@nhsdigital/eps-cdk-constructs" import { ApiFunctions } from "../resources/Functions" import { addNagSuppressions } from "./nagSuppression" +import { Dynamodb } from "../resources/DynamoDb" export interface HackStackProps extends StackProps { readonly serviceName: string @@ -176,6 +177,10 @@ export class HackStack extends Stack { ] }) + const dyna = new Dynamodb(this, "Dynamodb", { + stackName: props.stackName + }) + // Outputs // Exports From 6ca1e2aee05577e5a27c28747ef936d56915aa5d Mon Sep 17 00:00:00 2001 From: Anthony Brown Date: Tue, 10 Feb 2026 10:48:22 +0000 Subject: [PATCH 06/29] fix nag --- packages/cdk/stacks/nagSuppression.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/cdk/stacks/nagSuppression.ts b/packages/cdk/stacks/nagSuppression.ts index 82bd4a7..5d0b560 100644 --- a/packages/cdk/stacks/nagSuppression.ts +++ b/packages/cdk/stacks/nagSuppression.ts @@ -84,6 +84,26 @@ export const addNagSuppressions = (stack: Stack) => { } ] ) + safeAddNagSuppression( + stack, + "/HackStack/Dynamodb/ProcessStatusWriteManagedPolicy/Resource", + [ + { + id: "AwsSolutions-IAM5", + reason: "this is for hack day stack" + } + ] + ) + safeAddNagSuppression( + stack, + "/HackStack/Dynamodb/ProcessStatusReadManagedPolicy/Resource", + [ + { + id: "AwsSolutions-IAM5", + reason: "this is for hack day stack" + } + ] + ) } From c86a00787d8e2633ecaf7f833cb275e5929865ec Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 11:04:50 +0000 Subject: [PATCH 07/29] Fix package files --- package-lock.json | 24 +++++++++++++++++++++++- package.json | 3 ++- packages/create/package.json | 8 ++++---- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0af633c..8e9a95b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "packages/hack", "packages/cdk", "packages/foo", - "packages/process" + "packages/process", + "packages/create" ], "dependencies": { "conventional-changelog-eslint": "^6.0.0", @@ -7305,6 +7306,10 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/create": { + "resolved": "packages/create", + "link": true + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -15211,6 +15216,23 @@ "vite": "^7.3.1" } }, + "packages/create": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@aws-lambda-powertools/commons": "^2.30.2", + "@aws-lambda-powertools/logger": "^2.30.2", + "@aws-lambda-powertools/parameters": "^2.30.2", + "@middy/core": "^7.0.2", + "@middy/input-output-logger": "^7.0.2", + "@nhs/fhir-middy-error-handler": "^2.1.71", + "@nhsdigital/eps-spine-client": "^2.1.78" + }, + "devDependencies": { + "axios-mock-adapter": "^2.1.0", + "esbuild": "^0.27.2" + } + }, "packages/foo": { "name": "status", "version": "1.0.0", diff --git a/package.json b/package.json index 4f6e184..b10dd8a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "packages/hack", "packages/cdk", "packages/foo", - "packages/process" + "packages/process", + "packages/create" ], "devDependencies": { "@eslint/js": "^9.38.0", diff --git a/packages/create/package.json b/packages/create/package.json index d78bcbc..0cd473b 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -1,8 +1,8 @@ { - "name": "status", + "name": "create", "version": "1.0.0", - "description": "Lambda of the _status endpoint", - "main": "status.js", + "description": "Lambda of the create endpoint", + "main": "handler.js", "author": "NHS Digital", "license": "MIT", "scripts": { @@ -25,4 +25,4 @@ "axios-mock-adapter": "^2.1.0", "esbuild": "^0.27.2" } -} \ No newline at end of file +} From 2ba9dfaacc82d6ed2374719c17f010f18f9a35c6 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 11:16:22 +0000 Subject: [PATCH 08/29] create the uuid and return it --- packages/create/package.json | 1 + packages/create/src/handler.ts | 35 +++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/create/package.json b/packages/create/package.json index 0cd473b..ae40f08 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -16,6 +16,7 @@ "@aws-lambda-powertools/commons": "^2.30.2", "@aws-lambda-powertools/logger": "^2.30.2", "@aws-lambda-powertools/parameters": "^2.30.2", + "@aws-sdk/client-lambda": "^3.986.0", "@middy/core": "^7.0.2", "@middy/input-output-logger": "^7.0.2", "@nhs/fhir-middy-error-handler": "^2.1.71", diff --git a/packages/create/src/handler.ts b/packages/create/src/handler.ts index d3996bd..28e8a11 100644 --- a/packages/create/src/handler.ts +++ b/packages/create/src/handler.ts @@ -1,20 +1,24 @@ import {Logger} from "@aws-lambda-powertools/logger" +import {InvokeCommand, LambdaClient, LogType} from "@aws-sdk/client-lambda" import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" import middy from "@middy/core" import inputOutputLogger from "@middy/input-output-logger" import errorHandler from "@nhs/fhir-middy-error-handler" +import {randomUUID, UUID} from "node:crypto" + const logger = new Logger({serviceName: "status"}) -/** - * - * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format - * @param {Object} _event - API Gateway Lambda Proxy Input Format - * - * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html - * @returns {Object} object - API Gateway Lambda Proxy Output Format - * - */ +const invoke = async (funcName: string, payload: any) => { + const client = new LambdaClient({}); + const command = new InvokeCommand({ + FunctionName: funcName, + Payload: JSON.stringify(payload) + }); + + // We don't care about the response, so don't await this + client.send(command) +}; const lambdaHandler = async (event: any): Promise => { logger.appendKeys({ @@ -25,15 +29,16 @@ const lambdaHandler = async (event: any): Promise => { "apigw-request-id": event.requestContext.requestId }) - const commitId = process.env.COMMIT_ID - const versionNumber = process.env.VERSION_NUMBER - + // Create an empty record in dynamo with a new uuid + const uuid: UUID = randomUUID() + invoke(process.env.PROCESSING_LAMBDA_NAME!, {id: uuid}) - const statusBody = {commitId: commitId, versionNumber: versionNumber} + // immediately return 200 and the newly created ID + const createBody = {id: uuid} return { statusCode: 200, - body: JSON.stringify(statusBody), + body: JSON.stringify(createBody), headers: { "Content-Type": "application/health+json", "Cache-Control": "no-cache" @@ -50,4 +55,4 @@ export const handler = middy(lambdaHandler) } }) ) - .use(errorHandler({logger: logger})) \ No newline at end of file + .use(errorHandler({logger: logger})) From 042f56ac363d75e74bab053e54d044b6bee2dab1 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 11:17:13 +0000 Subject: [PATCH 09/29] missed some files --- package-lock.json | 292 ++++++++++++++++++++------------ packages/process/src/handler.ts | 2 +- 2 files changed, 183 insertions(+), 111 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e9a95b..698ed5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -436,6 +436,77 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-lambda": { + "version": "3.986.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.986.0.tgz", + "integrity": "sha512-R0VrqSH622b0MmIULLCNbupyU9qqEn+vofIeKng+ALPJY6U7pq7MG0p+bbxLCGztLl0u2vmO237SPZYcFm3hCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/credential-provider-node": "^3.972.6", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.986.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.5", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.1", + "@smithy/eventstream-serde-browser": "^4.2.8", + "@smithy/eventstream-serde-config-resolver": "^4.3.8", + "@smithy/eventstream-serde-node": "^4.2.8", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/middleware-retry": "^4.4.30", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.9", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.29", + "@smithy/util-defaults-mode-node": "^4.2.32", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-stream": "^4.5.11", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/@aws-sdk/util-endpoints": { + "version": "3.986.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.986.0.tgz", + "integrity": "sha512-Mqi79L38qi1gCG3adlVdbNrSxvcm1IPDLiJPA3OBypY5ewxUyWbaA3DD4goG+EwET6LSFgZJcRSIh6KBNpP5pA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-route-53": { "version": "3.984.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-route-53/-/client-route-53-3.984.0.tgz", @@ -555,44 +626,44 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.982.0.tgz", - "integrity": "sha512-qJrIiivmvujdGqJ0ldSUvhN3k3N7GtPesoOI1BSt0fNXovVnMz4C/JmnkhZihU7hJhDvxJaBROLYTU+lpild4w==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.985.0.tgz", + "integrity": "sha512-81J8iE8MuXhdbMfIz4sWFj64Pe41bFi/uqqmqOC5SlGv+kwoyLsyKS/rH2tW2t5buih4vTUxskRjxlqikTD4oQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", + "@aws-sdk/core": "^3.973.7", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/middleware-user-agent": "^3.972.7", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", + "@aws-sdk/util-endpoints": "3.985.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", + "@aws-sdk/util-user-agent-node": "^3.972.5", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", + "@smithy/core": "^3.22.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/middleware-retry": "^4.4.30", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.9", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-defaults-mode-browser": "^4.3.29", + "@smithy/util-defaults-mode-node": "^4.2.32", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -604,9 +675,9 @@ } }, "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/util-endpoints": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", - "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz", + "integrity": "sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -620,19 +691,19 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.6.tgz", - "integrity": "sha512-pz4ZOw3BLG0NdF25HoB9ymSYyPbMiIjwQJ2aROXRhAzt+b+EOxStfFv8s5iZyP6Kiw7aYhyWxj5G3NhmkoOTKw==", + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.7.tgz", + "integrity": "sha512-wNZZQQNlJ+hzD49cKdo+PY6rsTDElO8yDImnrI69p2PLBa7QomeUKAJWYp9xnaR38nlHqWhMHZuYLCQ3oSX+xg==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.4", - "@smithy/core": "^3.22.0", + "@smithy/core": "^3.22.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", @@ -657,12 +728,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.4.tgz", - "integrity": "sha512-/8dnc7+XNMmViEom2xsNdArQxQPSgy4Z/lm6qaFPTrMFesT1bV3PsBhb19n09nmxHdrtQskYmViddUIjUQElXg==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.5.tgz", + "integrity": "sha512-LxJ9PEO4gKPXzkufvIESUysykPIdrV7+Ocb9yAhbhJLE4TiAYqbCVUE+VuKP1leGR1bBfjWjYgSV5MxprlX3mQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", + "@aws-sdk/core": "^3.973.7", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", @@ -673,20 +744,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.6.tgz", - "integrity": "sha512-5ERWqRljiZv44AIdvIRQ3k+EAV0Sq2WeJHvXuK7gL7bovSxOf8Al7MLH7Eh3rdovH4KHFnlIty7J71mzvQBl5Q==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.7.tgz", + "integrity": "sha512-L2uOGtvp2x3bTcxFTpSM+GkwFIPd8pHfGWO1764icMbo7e5xJh0nfhx1UwkXLnwvocTNEf8A7jISZLYjUSNaTg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", + "@aws-sdk/core": "^3.973.7", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.9", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@smithy/util-stream": "^4.5.11", "tslib": "^2.6.2" }, "engines": { @@ -694,19 +765,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.4.tgz", - "integrity": "sha512-eRUg+3HaUKuXWn/lEMirdiA5HOKmEl8hEHVuszIDt2MMBUKgVX5XNGmb3XmbgU17h6DZ+RtjbxQpjhz3SbTjZg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/credential-provider-env": "^3.972.4", - "@aws-sdk/credential-provider-http": "^3.972.6", - "@aws-sdk/credential-provider-login": "^3.972.4", - "@aws-sdk/credential-provider-process": "^3.972.4", - "@aws-sdk/credential-provider-sso": "^3.972.4", - "@aws-sdk/credential-provider-web-identity": "^3.972.4", - "@aws-sdk/nested-clients": "3.982.0", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.5.tgz", + "integrity": "sha512-SdDTYE6jkARzOeL7+kudMIM4DaFnP5dZVeatzw849k4bSXDdErDS188bgeNzc/RA2WGrlEpsqHUKP6G7sVXhZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/credential-provider-env": "^3.972.5", + "@aws-sdk/credential-provider-http": "^3.972.7", + "@aws-sdk/credential-provider-login": "^3.972.5", + "@aws-sdk/credential-provider-process": "^3.972.5", + "@aws-sdk/credential-provider-sso": "^3.972.5", + "@aws-sdk/credential-provider-web-identity": "^3.972.5", + "@aws-sdk/nested-clients": "3.985.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", @@ -719,13 +790,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.4.tgz", - "integrity": "sha512-nLGjXuvWWDlQAp505xIONI7Gam0vw2p7Qu3P6on/W2q7rjJXtYjtpHbcsaOjJ/pAju3eTvEQuSuRedcRHVQIAQ==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.5.tgz", + "integrity": "sha512-uYq1ILyTSI6ZDCMY5+vUsRM0SOCVI7kaW4wBrehVVkhAxC6y+e9rvGtnoZqCOWL1gKjTMouvsf4Ilhc5NCg1Aw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/nested-clients": "3.985.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", @@ -738,17 +809,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.5.tgz", - "integrity": "sha512-VWXKgSISQCI2GKN3zakTNHSiZ0+mux7v6YHmmbLQp/o3fvYUQJmKGcLZZzg2GFA+tGGBStplra9VFNf/WwxpYg==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.6.tgz", + "integrity": "sha512-DZ3CnAAtSVtVz+G+ogqecaErMLgzph4JH5nYbHoBMgBkwTUV+SUcjsjOJwdBJTHu3Dm6l5LBYekZoU2nDqQk2A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.4", - "@aws-sdk/credential-provider-http": "^3.972.6", - "@aws-sdk/credential-provider-ini": "^3.972.4", - "@aws-sdk/credential-provider-process": "^3.972.4", - "@aws-sdk/credential-provider-sso": "^3.972.4", - "@aws-sdk/credential-provider-web-identity": "^3.972.4", + "@aws-sdk/credential-provider-env": "^3.972.5", + "@aws-sdk/credential-provider-http": "^3.972.7", + "@aws-sdk/credential-provider-ini": "^3.972.5", + "@aws-sdk/credential-provider-process": "^3.972.5", + "@aws-sdk/credential-provider-sso": "^3.972.5", + "@aws-sdk/credential-provider-web-identity": "^3.972.5", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", @@ -761,12 +832,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.4.tgz", - "integrity": "sha512-TCZpWUnBQN1YPk6grvd5x419OfXjHvhj5Oj44GYb84dOVChpg/+2VoEj+YVA4F4E/6huQPNnX7UYbTtxJqgihw==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.5.tgz", + "integrity": "sha512-HDKF3mVbLnuqGg6dMnzBf1VUOywE12/N286msI9YaK9mEIzdsGCtLTvrDhe3Up0R9/hGFbB+9l21/TwF5L1C6g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", + "@aws-sdk/core": "^3.973.7", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -778,14 +849,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.4.tgz", - "integrity": "sha512-wzsGwv9mKlwJ3vHLyembBvGE/5nPUIwRR2I51B1cBV4Cb4ql9nIIfpmHzm050XYTY5fqTOKJQnhLj7zj89VG8g==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.5.tgz", + "integrity": "sha512-8urj3AoeNeQisjMmMBhFeiY2gxt6/7wQQbEGun0YV/OaOOiXrIudTIEYF8ZfD+NQI6X1FY5AkRsx6O/CaGiybA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.982.0", - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/token-providers": "3.982.0", + "@aws-sdk/client-sso": "3.985.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/token-providers": "3.985.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -797,13 +868,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.4.tgz", - "integrity": "sha512-hIzw2XzrG8jzsUSEatehmpkd5rWzASg5IHUfA+m01k/RtvfAML7ZJVVohuKdhAYx+wV2AThLiQJVzqn7F0khrw==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.5.tgz", + "integrity": "sha512-OK3cULuJl6c+RcDZfPpaK5o3deTOnKZbxm7pzhFNGA3fI2hF9yDih17fGRazJzGGWaDVlR9ejZrpDef4DJCEsw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/nested-clients": "3.985.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -985,15 +1056,15 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.6.tgz", - "integrity": "sha512-TehLN8W/kivl0U9HcS+keryElEWORROpghDXZBLfnb40DXM7hx/i+7OOjkogXQOF3QtUraJVRkHQ07bPhrWKlw==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.7.tgz", + "integrity": "sha512-HUD+geASjXSCyL/DHPQc/Ua7JhldTcIglVAoCV8kiVm99IaFSlAbTvEnyhZwdE6bdFyTL+uIaWLaCFSRsglZBQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", + "@aws-sdk/core": "^3.973.7", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", - "@smithy/core": "^3.22.0", + "@aws-sdk/util-endpoints": "3.985.0", + "@smithy/core": "^3.22.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" @@ -1003,9 +1074,9 @@ } }, "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", - "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz", + "integrity": "sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -1019,44 +1090,44 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", - "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.985.0.tgz", + "integrity": "sha512-TsWwKzb/2WHafAY0CE7uXgLj0FmnkBTgfioG9HO+7z/zCPcl1+YU+i7dW4o0y+aFxFgxTMG+ExBQpqT/k2ao8g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.6", + "@aws-sdk/core": "^3.973.7", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/middleware-user-agent": "^3.972.7", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.982.0", + "@aws-sdk/util-endpoints": "3.985.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.4", + "@aws-sdk/util-user-agent-node": "^3.972.5", "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.22.0", + "@smithy/core": "^3.22.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.12", - "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/middleware-retry": "^4.4.30", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", + "@smithy/node-http-handler": "^4.4.9", "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.1", + "@smithy/smithy-client": "^4.11.2", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.28", - "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-defaults-mode-browser": "^4.3.29", + "@smithy/util-defaults-mode-node": "^4.2.32", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", @@ -1068,9 +1139,9 @@ } }, "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", - "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.985.0.tgz", + "integrity": "sha512-vth7UfGSUR3ljvaq8V4Rc62FsM7GUTH/myxPWkaEgOrprz1/Pc72EgTXxj+cPPPDAfHFIpjhkB7T7Td0RJx+BA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.1", @@ -1117,13 +1188,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.982.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.982.0.tgz", - "integrity": "sha512-v3M0KYp2TVHYHNBT7jHD9lLTWAdS9CaWJ2jboRKt0WAB65bA7iUEpR+k4VqKYtpQN4+8kKSc4w+K6kUNZkHKQw==", + "version": "3.985.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.985.0.tgz", + "integrity": "sha512-+hwpHZyEq8k+9JL2PkE60V93v2kNhUIv7STFt+EAez1UJsJOQDhc5LpzEX66pNjclI5OTwBROs/DhJjC/BtMjQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.6", - "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/nested-clients": "3.985.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", @@ -1200,12 +1271,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.4.tgz", - "integrity": "sha512-3WFCBLiM8QiHDfosQq3Py+lIMgWlFWwFQliUHUqwEiRqLnKyhgbU3AKa7AWJF7lW2Oc/2kFNY4MlAYVnVc0i8A==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.5.tgz", + "integrity": "sha512-GsUDF+rXyxDZkkJxUsDxnA67FG+kc5W1dnloCFLl6fWzceevsCYzJpASBzT+BPjwUgREE6FngfJYYYMQUY5fZQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/middleware-user-agent": "^3.972.7", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", @@ -15223,6 +15294,7 @@ "@aws-lambda-powertools/commons": "^2.30.2", "@aws-lambda-powertools/logger": "^2.30.2", "@aws-lambda-powertools/parameters": "^2.30.2", + "@aws-sdk/client-lambda": "^3.986.0", "@middy/core": "^7.0.2", "@middy/input-output-logger": "^7.0.2", "@nhs/fhir-middy-error-handler": "^2.1.71", diff --git a/packages/process/src/handler.ts b/packages/process/src/handler.ts index 1cf0dec..a70e521 100644 --- a/packages/process/src/handler.ts +++ b/packages/process/src/handler.ts @@ -56,4 +56,4 @@ export const handler = middy(lambdaHandler) } }) ) - .use(errorHandler({logger: logger})) \ No newline at end of file + .use(errorHandler({logger: logger})) From 20e8dfee40194a4271e080e79b15808328e0941f Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 11:20:00 +0000 Subject: [PATCH 10/29] Try a GET --- packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts b/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts index 3d17022..bd5e40c 100644 --- a/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts +++ b/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts @@ -42,7 +42,7 @@ export class RestApiGatewayMethods extends Construct { }) const createPrescriptionLambdaResource = props.restApiGateway.root.addResource("create") - createPrescriptionLambdaResource.addMethod("POST", new LambdaIntegration(props.createLambda, { + createPrescriptionLambdaResource.addMethod("GET", new LambdaIntegration(props.createLambda, { credentialsRole: props.restAPiGatewayRole }), { authorizationType: AuthorizationType.NONE, From c23e16338541819497a928e4f346a3d4de758824 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 11:23:19 +0000 Subject: [PATCH 11/29] add nag suppressions --- packages/cdk/stacks/nagSuppression.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/cdk/stacks/nagSuppression.ts b/packages/cdk/stacks/nagSuppression.ts index 5d0b560..9b7a460 100644 --- a/packages/cdk/stacks/nagSuppression.ts +++ b/packages/cdk/stacks/nagSuppression.ts @@ -104,6 +104,26 @@ export const addNagSuppressions = (stack: Stack) => { } ] ) + safeAddNagSuppression( + stack, + "/HackStack/ApiGateway/ApiGateway/Default/create/GET/Resource", + [ + { + id: "AwsSolutions-APIG4", + reason: "this is for hack day stack - no auth is fine (for now)" + } + ] + ) + safeAddNagSuppression( + stack, + "/HackStack/ApiGateway/ApiGateway/Default/create/GET/Resource", + [ + { + id: "AwsSolutions-COG4", + reason: "this is for hack day stack - no auth is fine" + } + ] + ) } From 608a9687e244a5223f072dea220f2101eda3a203 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 11:26:25 +0000 Subject: [PATCH 12/29] pass the process lambda name to the create lambda env --- packages/cdk/resources/Functions.ts | 17 +++++++++-------- packages/create/src/handler.ts | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/cdk/resources/Functions.ts b/packages/cdk/resources/Functions.ts index 48ca4cf..cafcc4b 100644 --- a/packages/cdk/resources/Functions.ts +++ b/packages/cdk/resources/Functions.ts @@ -32,10 +32,10 @@ export class ApiFunctions extends Construct { commitId: props.commitId }) - const createLambda = new TypescriptLambdaFunction(this, "CreateLambda", { - functionName: `${props.stackName}-CreateLambda`, + const processLambda = new TypescriptLambdaFunction(this, "ProcessLambda", { + functionName: `${props.stackName}-ProcessLambda`, projectBaseDir: baseDir, - packageBasePath: "packages/create", + packageBasePath: "packages/process", entryPoint: "src/handler.ts", environmentVariables: {}, logRetentionInDays: 30, @@ -44,19 +44,20 @@ export class ApiFunctions extends Construct { commitId: props.commitId }) - const processLambda = new TypescriptLambdaFunction(this, "ProcessLambda", { - functionName: `${props.stackName}-ProcessLambda`, + const createLambda = new TypescriptLambdaFunction(this, "CreateLambda", { + functionName: `${props.stackName}-CreateLambda`, projectBaseDir: baseDir, - packageBasePath: "packages/process", + packageBasePath: "packages/create", entryPoint: "src/handler.ts", - environmentVariables: {}, + environmentVariables: { + PROCESSING_LAMBDA_NAME: processLambda.function.functionName + }, logRetentionInDays: 30, logLevel: "DEBUG", version: props.version, commitId: props.commitId }) - // Outputs this.fooLambda = fooLambda this.createLambda = createLambda diff --git a/packages/create/src/handler.ts b/packages/create/src/handler.ts index 28e8a11..6c77b8e 100644 --- a/packages/create/src/handler.ts +++ b/packages/create/src/handler.ts @@ -7,7 +7,7 @@ import errorHandler from "@nhs/fhir-middy-error-handler" import {randomUUID, UUID} from "node:crypto" -const logger = new Logger({serviceName: "status"}) +const logger = new Logger({serviceName: "create"}) const invoke = async (funcName: string, payload: any) => { const client = new LambdaClient({}); From 843488560a8ad9dea077ac690b55b99791890fd2 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 11:39:14 +0000 Subject: [PATCH 13/29] Add policies --- packages/cdk/resources/Functions.ts | 24 +++++++++++++++++++++++- packages/cdk/stacks/HackStack.ts | 9 +++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/cdk/resources/Functions.ts b/packages/cdk/resources/Functions.ts index cafcc4b..2da491c 100644 --- a/packages/cdk/resources/Functions.ts +++ b/packages/cdk/resources/Functions.ts @@ -1,6 +1,7 @@ import {Construct} from "constructs" import {TypescriptLambdaFunction} from "@nhsdigital/eps-cdk-constructs" import {resolve} from "node:path" +import {Dynamodb} from "./DynamoDb" const baseDir = resolve(__dirname, "../../..") // Interface for properties needed to create API functions export interface ApiFunctionsProps { @@ -8,6 +9,7 @@ export interface ApiFunctionsProps { readonly stackName: string readonly version: string readonly commitId: string + readonly processingStatusTable: Dynamodb } /** @@ -17,6 +19,7 @@ export class ApiFunctions extends Construct { public readonly fooLambda: TypescriptLambdaFunction public readonly createLambda: TypescriptLambdaFunction public readonly processLambda: TypescriptLambdaFunction + public readonly pollLambda: TypescriptLambdaFunction public constructor(scope: Construct, id: string, props: ApiFunctionsProps) { super(scope, id) @@ -55,12 +58,31 @@ export class ApiFunctions extends Construct { logRetentionInDays: 30, logLevel: "DEBUG", version: props.version, - commitId: props.commitId + commitId: props.commitId, + additionalPolicies: [ + processLambda.executionPolicy + ] + }) + + const pollLambda = new TypescriptLambdaFunction(this, "PollLambda", { + functionName: `${props.stackName}-PollLambda`, + projectBaseDir: baseDir, + packageBasePath: "packages/poll", + entryPoint: "src/handler.ts", + environmentVariables: {}, + logRetentionInDays: 30, + logLevel: "DEBUG", + version: props.version, + commitId: props.commitId, + additionalPolicies: [ + props.processingStatusTable.processStatusTableReadPolicy, + ] }) // Outputs this.fooLambda = fooLambda this.createLambda = createLambda this.processLambda = processLambda + this.pollLambda = pollLambda } } diff --git a/packages/cdk/stacks/HackStack.ts b/packages/cdk/stacks/HackStack.ts index d0d81e0..38f4298 100644 --- a/packages/cdk/stacks/HackStack.ts +++ b/packages/cdk/stacks/HackStack.ts @@ -94,6 +94,9 @@ export class HackStack extends Stack { wafLogGroupName: `aws-waf-logs-${props.serviceName}-apigw`, stackName: this.stackName }) + const dyna = new Dynamodb(this, "Dynamodb", { + stackName: props.stackName + }) const apiGateway = new RestApiGateway(this, "ApiGateway", { serviceName: props.serviceName, stackName: props.stackName, @@ -107,7 +110,8 @@ export class HackStack extends Stack { serviceName: props.serviceName, stackName: props.stackName, version: "1.0.0", - commitId: "abc123" + commitId: "abc123", + processingStatusTable: dyna }) const apiMethods = new RestApiGatewayMethods(this, "RestApiGatewayMethods", { executePolices: [ @@ -178,9 +182,6 @@ export class HackStack extends Stack { ] }) - const dyna = new Dynamodb(this, "Dynamodb", { - stackName: props.stackName - }) // Outputs From 322d953a55f7f698f8ebba865e2ebc7cd7be7d1a Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 11:44:13 +0000 Subject: [PATCH 14/29] Add the handler for the poll lambda --- package-lock.json | 24 +++++++++++++++++++- package.json | 3 ++- packages/poll/package.json | 28 +++++++++++++++++++++++ packages/poll/src/handler.ts | 44 ++++++++++++++++++++++++++++++++++++ packages/poll/tsconfig.json | 9 ++++++++ 5 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/poll/package.json create mode 100644 packages/poll/src/handler.ts create mode 100644 packages/poll/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 698ed5d..1d5ee6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "packages/cdk", "packages/foo", "packages/process", - "packages/create" + "packages/create", + "packages/poll" ], "dependencies": { "conventional-changelog-eslint": "^6.0.0", @@ -12301,6 +12302,10 @@ "pathe": "^2.0.3" } }, + "node_modules/poll": { + "resolved": "packages/poll", + "link": true + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -15622,6 +15627,23 @@ "dev": true, "license": "MIT" }, + "packages/poll": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@aws-lambda-powertools/commons": "^2.30.2", + "@aws-lambda-powertools/logger": "^2.30.2", + "@aws-lambda-powertools/parameters": "^2.30.2", + "@middy/core": "^7.0.2", + "@middy/input-output-logger": "^7.0.2", + "@nhs/fhir-middy-error-handler": "^2.1.71", + "@nhsdigital/eps-spine-client": "^2.1.78" + }, + "devDependencies": { + "axios-mock-adapter": "^2.1.0", + "esbuild": "^0.27.2" + } + }, "packages/process": { "version": "1.0.0", "license": "MIT", diff --git a/package.json b/package.json index b10dd8a..43a4c50 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "packages/cdk", "packages/foo", "packages/process", - "packages/create" + "packages/create", + "packages/poll" ], "devDependencies": { "@eslint/js": "^9.38.0", diff --git a/packages/poll/package.json b/packages/poll/package.json new file mode 100644 index 0000000..3ea9a9c --- /dev/null +++ b/packages/poll/package.json @@ -0,0 +1,28 @@ +{ + "name": "poll", + "version": "1.0.0", + "description": "Lambda of the poll endpoint", + "main": "handler.js", + "author": "NHS Digital", + "license": "MIT", + "scripts": { + "unit": "POWERTOOLS_DEV=true NODE_OPTIONS=--experimental-vm-modules jest --no-cache --coverage", + "lint": "eslint --max-warnings 0 --fix --config ../../eslint.config.mjs .", + "compile": "tsc --build", + "test": "npm run compile && npm run unit", + "check-licenses": "license-checker --failOn GPL --failOn LGPL --start ../.." + }, + "dependencies": { + "@aws-lambda-powertools/commons": "^2.30.2", + "@aws-lambda-powertools/logger": "^2.30.2", + "@aws-lambda-powertools/parameters": "^2.30.2", + "@middy/core": "^7.0.2", + "@middy/input-output-logger": "^7.0.2", + "@nhs/fhir-middy-error-handler": "^2.1.71", + "@nhsdigital/eps-spine-client": "^2.1.78" + }, + "devDependencies": { + "axios-mock-adapter": "^2.1.0", + "esbuild": "^0.27.2" + } +} diff --git a/packages/poll/src/handler.ts b/packages/poll/src/handler.ts new file mode 100644 index 0000000..74d11cd --- /dev/null +++ b/packages/poll/src/handler.ts @@ -0,0 +1,44 @@ +import {Logger} from "@aws-lambda-powertools/logger" +import {injectLambdaContext} from "@aws-lambda-powertools/logger/middleware" +import middy from "@middy/core" +import inputOutputLogger from "@middy/input-output-logger" +import errorHandler from "@nhs/fhir-middy-error-handler" + +const logger = new Logger({serviceName: "poll"}) + + +const lambdaHandler = async (event: any): Promise => { + logger.appendKeys({ + "nhsd-correlation-id": event.headers["nhsd-correlation-id"], + "x-request-id": event.headers["x-request-id"], + "nhsd-request-id": event.headers["nhsd-request-id"], + "x-correlation-id": event.headers["x-correlation-id"], + "apigw-request-id": event.requestContext.requestId + }) + + const commitId = process.env.COMMIT_ID + const versionNumber = process.env.VERSION_NUMBER + + + const statusBody = {commitId: commitId, versionNumber: versionNumber} + + return { + statusCode: 200, + body: JSON.stringify(statusBody), + headers: { + "Content-Type": "application/health+json", + "Cache-Control": "no-cache" + } + } +} + +export const handler = middy(lambdaHandler) + .use(injectLambdaContext(logger, {clearState: true})) + .use( + inputOutputLogger({ + logger: (request) => { + logger.info(request) + } + }) + ) + .use(errorHandler({logger: logger})) diff --git a/packages/poll/tsconfig.json b/packages/poll/tsconfig.json new file mode 100644 index 0000000..d26c8f0 --- /dev/null +++ b/packages/poll/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.defaults.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "lib" + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file From 21beeb34496907d7ea8df2f605430230bd515ee8 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 11:59:26 +0000 Subject: [PATCH 15/29] Add dynamo interface --- package-lock.json | 133 ++++++++++++++++++++++++++++ packages/cdk/resources/Functions.ts | 4 +- packages/poll/package.json | 1 + packages/poll/src/dynamo.ts | 60 +++++++++++++ packages/poll/src/handler.ts | 24 +++-- 5 files changed, 216 insertions(+), 6 deletions(-) create mode 100644 packages/poll/src/dynamo.ts diff --git a/package-lock.json b/package-lock.json index 1d5ee6a..444e72b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -437,6 +437,76 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.986.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.986.0.tgz", + "integrity": "sha512-4SPBE+QzRl8Yi8mSHDahwE+rKgCB1RhiIYoeqfwpkiocXnMaBQsNSEaJaschLTbC6cOPs2RqM1/oIF50do/OvA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.7", + "@aws-sdk/credential-provider-node": "^3.972.6", + "@aws-sdk/dynamodb-codec": "^3.972.8", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.3", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.7", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.986.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.5", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.1", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/middleware-retry": "^4.4.30", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.9", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.29", + "@smithy/util-defaults-mode-node": "^4.2.32", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb/node_modules/@aws-sdk/util-endpoints": { + "version": "3.986.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.986.0.tgz", + "integrity": "sha512-Mqi79L38qi1gCG3adlVdbNrSxvcm1IPDLiJPA3OBypY5ewxUyWbaA3DD4goG+EwET6LSFgZJcRSIh6KBNpP5pA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-lambda": { "version": "3.986.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.986.0.tgz", @@ -886,6 +956,36 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/dynamodb-codec": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.972.8.tgz", + "integrity": "sha512-5ngfn6fQPSNc7G9LlingK4SXfzcJtv5pOP++erc7HmCq0LcDj//0pcpLgxpDII0sBTh0FcR/iw9i4fBZwSJ2Cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.7", + "@smithy/core": "^3.22.1", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.2.tgz", + "integrity": "sha512-3L7mwqSLJ6ouZZKtCntoNF0HTYDNs1FDQqkGjoPWXcv1p0gnLotaDmLq1rIDqfu4ucOit0Re3ioLyYDUTpSroA==", + "license": "Apache-2.0", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz", @@ -904,6 +1004,23 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.3.tgz", + "integrity": "sha512-xAxA8/TOygQmMrzcw9CrlpTHCGWSG/lvzrHCySfSZpDN4/yVSfXO+gUwW9WxeskBmuv9IIFATOVpzc9EzfTZ0Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/endpoint-cache": "^3.972.2", + "@aws-sdk/types": "^3.973.1", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/middleware-expect-continue": { "version": "3.972.3", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz", @@ -11520,6 +11637,15 @@ "node": ">=10" } }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "license": "MIT", + "dependencies": { + "obliterator": "^1.6.1" + } + }, "node_modules/morgan": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", @@ -11866,6 +11992,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "license": "MIT" + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -15634,6 +15766,7 @@ "@aws-lambda-powertools/commons": "^2.30.2", "@aws-lambda-powertools/logger": "^2.30.2", "@aws-lambda-powertools/parameters": "^2.30.2", + "@aws-sdk/client-dynamodb": "^3.986.0", "@middy/core": "^7.0.2", "@middy/input-output-logger": "^7.0.2", "@nhs/fhir-middy-error-handler": "^2.1.71", diff --git a/packages/cdk/resources/Functions.ts b/packages/cdk/resources/Functions.ts index 2da491c..cdf8f47 100644 --- a/packages/cdk/resources/Functions.ts +++ b/packages/cdk/resources/Functions.ts @@ -69,7 +69,9 @@ export class ApiFunctions extends Construct { projectBaseDir: baseDir, packageBasePath: "packages/poll", entryPoint: "src/handler.ts", - environmentVariables: {}, + environmentVariables: { + PROCESSING_STATUS_TABLE_NAME: props.processingStatusTable.processStatus.tableName + }, logRetentionInDays: 30, logLevel: "DEBUG", version: props.version, diff --git a/packages/poll/package.json b/packages/poll/package.json index 3ea9a9c..bf7efb9 100644 --- a/packages/poll/package.json +++ b/packages/poll/package.json @@ -16,6 +16,7 @@ "@aws-lambda-powertools/commons": "^2.30.2", "@aws-lambda-powertools/logger": "^2.30.2", "@aws-lambda-powertools/parameters": "^2.30.2", + "@aws-sdk/client-dynamodb": "^3.986.0", "@middy/core": "^7.0.2", "@middy/input-output-logger": "^7.0.2", "@nhs/fhir-middy-error-handler": "^2.1.71", diff --git a/packages/poll/src/dynamo.ts b/packages/poll/src/dynamo.ts new file mode 100644 index 0000000..9e7a980 --- /dev/null +++ b/packages/poll/src/dynamo.ts @@ -0,0 +1,60 @@ +import {Logger} from "@aws-lambda-powertools/logger" +import {DynamoDBClient, QueryCommand, QueryCommandInput} from "@aws-sdk/client-dynamodb" + +const client = new DynamoDBClient() +const tableName = process.env.TABLE_NAME ?? "PrescriptionStatusUpdates" + +export async function queryActionState( + actionID: string, + logger: Logger +): Promise> { + + // Use the GSI to query by PrescriptionID + const query: QueryCommandInput = { + TableName: tableName, + KeyConditionExpression: "actionId = :aid", + ExpressionAttributeValues: { + ":aid": {S: actionID} + } + } + + let lastEvaluatedKey + let items = [] + + logger.info("Querying DynamoDB for action ID", { + actionID, + tableName, + }) + + try { + while (true) { + if (lastEvaluatedKey) { + query.ExclusiveStartKey = lastEvaluatedKey + } + + const result = await client.send(new QueryCommand(query)) + + if (result.Items) { + items.push(...result.Items) + } + + lastEvaluatedKey = result.LastEvaluatedKey + if (!lastEvaluatedKey) { + break + } + } + + logger.info("Retrieved records from DynamoDB", { + actionID, + recordCount: items.length + }) + + return items + } catch (err) { + logger.error("Error querying DynamoDB for existing prescription records", { + actionID, + error: err + }) + throw err + } +} diff --git a/packages/poll/src/handler.ts b/packages/poll/src/handler.ts index 74d11cd..1f9a61b 100644 --- a/packages/poll/src/handler.ts +++ b/packages/poll/src/handler.ts @@ -4,8 +4,11 @@ import middy from "@middy/core" import inputOutputLogger from "@middy/input-output-logger" import errorHandler from "@nhs/fhir-middy-error-handler" +import {queryActionState} from "./dynamo" + const logger = new Logger({serviceName: "poll"}) +const tableName = process.env.PROCESSING_STATUS_TABLE_NAME! const lambdaHandler = async (event: any): Promise => { logger.appendKeys({ @@ -16,15 +19,26 @@ const lambdaHandler = async (event: any): Promise => { "apigw-request-id": event.requestContext.requestId }) - const commitId = process.env.COMMIT_ID - const versionNumber = process.env.VERSION_NUMBER - + // take the action ID from the query string parameters and use it to query the DynamoDB table for any existing records with that action ID + const actionID = event.queryStringParameters?.actionId + + if (!actionID) { + logger.warn("No action ID provided in query parameters") + return { + statusCode: 400, + body: JSON.stringify({message: "Missing required query parameter: actionId"}), + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-cache" + } + } + } - const statusBody = {commitId: commitId, versionNumber: versionNumber} + const result = await queryActionState(actionID, logger) return { statusCode: 200, - body: JSON.stringify(statusBody), + body: JSON.stringify(result), headers: { "Content-Type": "application/health+json", "Cache-Control": "no-cache" From e47da21dde8cd55304a01aaf8d2a4de8e6654744 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 12:01:40 +0000 Subject: [PATCH 16/29] add permission to api gateway --- packages/cdk/stacks/HackStack.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cdk/stacks/HackStack.ts b/packages/cdk/stacks/HackStack.ts index 38f4298..56546b0 100644 --- a/packages/cdk/stacks/HackStack.ts +++ b/packages/cdk/stacks/HackStack.ts @@ -115,7 +115,8 @@ export class HackStack extends Stack { }) const apiMethods = new RestApiGatewayMethods(this, "RestApiGatewayMethods", { executePolices: [ - functions.fooLambda.executionPolicy + functions.fooLambda.executionPolicy, + functions.createLambda.executionPolicy ], restAPiGatewayRole: apiGateway.apiGatewayRole, restApiGateway: apiGateway.apiGateway, From 38d95216cef924f2f5211404b4d529cee4c9afae Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 13:13:00 +0000 Subject: [PATCH 17/29] add a log --- packages/create/src/handler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/create/src/handler.ts b/packages/create/src/handler.ts index 6c77b8e..fe23890 100644 --- a/packages/create/src/handler.ts +++ b/packages/create/src/handler.ts @@ -31,6 +31,7 @@ const lambdaHandler = async (event: any): Promise => { // Create an empty record in dynamo with a new uuid const uuid: UUID = randomUUID() + logger.info("invoking processing lambda", {processingLambdaName: process.env.PROCESSING_LAMBDA_NAME, id: uuid}) invoke(process.env.PROCESSING_LAMBDA_NAME!, {id: uuid}) // immediately return 200 and the newly created ID From 8dbf570ce75e53ecea76baccd00f72dc7c9dbfae Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 13:16:00 +0000 Subject: [PATCH 18/29] proper invokation --- packages/create/src/handler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/create/src/handler.ts b/packages/create/src/handler.ts index fe23890..4665a4f 100644 --- a/packages/create/src/handler.ts +++ b/packages/create/src/handler.ts @@ -13,11 +13,11 @@ const invoke = async (funcName: string, payload: any) => { const client = new LambdaClient({}); const command = new InvokeCommand({ FunctionName: funcName, + InvocationType: "Event", // asynchronous invocation Payload: JSON.stringify(payload) }); - // We don't care about the response, so don't await this - client.send(command) + await client.send(command) }; const lambdaHandler = async (event: any): Promise => { @@ -32,7 +32,7 @@ const lambdaHandler = async (event: any): Promise => { // Create an empty record in dynamo with a new uuid const uuid: UUID = randomUUID() logger.info("invoking processing lambda", {processingLambdaName: process.env.PROCESSING_LAMBDA_NAME, id: uuid}) - invoke(process.env.PROCESSING_LAMBDA_NAME!, {id: uuid}) + await invoke(process.env.PROCESSING_LAMBDA_NAME!, {id: uuid}) // immediately return 200 and the newly created ID const createBody = {id: uuid} From cfa47d07c6beff388898bdec6771ca9408bd13e0 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 13:19:12 +0000 Subject: [PATCH 19/29] minor name change --- packages/poll/src/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/poll/src/handler.ts b/packages/poll/src/handler.ts index 1f9a61b..19f7dfd 100644 --- a/packages/poll/src/handler.ts +++ b/packages/poll/src/handler.ts @@ -20,7 +20,7 @@ const lambdaHandler = async (event: any): Promise => { }) // take the action ID from the query string parameters and use it to query the DynamoDB table for any existing records with that action ID - const actionID = event.queryStringParameters?.actionId + const actionID = event.queryStringParameters?.actionid if (!actionID) { logger.warn("No action ID provided in query parameters") From ab9cee72956a047879aae87a4b57307991513901 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Tue, 10 Feb 2026 14:30:52 +0000 Subject: [PATCH 20/29] Add poll API entry --- .../cdk/resources/RestApiGateway/RestApiGatewayMethods.ts | 8 ++++++++ packages/cdk/stacks/HackStack.ts | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts b/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts index bd5e40c..479e3fc 100644 --- a/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts +++ b/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts @@ -14,6 +14,7 @@ export interface RestApiGatewayMethodsProps { readonly restApiGateway: RestApi readonly fooLambda: NodejsFunction readonly createLambda: NodejsFunction + readonly pollLambda: NodejsFunction } /** @@ -48,6 +49,13 @@ export class RestApiGatewayMethods extends Construct { authorizationType: AuthorizationType.NONE, }) + const pollPrescriptionLambdaResource = props.restApiGateway.root.addResource("poll") + pollPrescriptionLambdaResource.addMethod("GET", new LambdaIntegration(props.pollLambda, { + credentialsRole: props.restAPiGatewayRole + }), { + authorizationType: AuthorizationType.NONE, + }) + //Outputs } } diff --git a/packages/cdk/stacks/HackStack.ts b/packages/cdk/stacks/HackStack.ts index 56546b0..e1e29ff 100644 --- a/packages/cdk/stacks/HackStack.ts +++ b/packages/cdk/stacks/HackStack.ts @@ -121,7 +121,8 @@ export class HackStack extends Stack { restAPiGatewayRole: apiGateway.apiGatewayRole, restApiGateway: apiGateway.apiGateway, fooLambda: functions.fooLambda.function, - createLambda: functions.createLambda.function + createLambda: functions.createLambda.function, + pollLambda: functions.pollLambda.function, }) const staticContentBucketOrigin = S3BucketOrigin.withOriginAccessControl( staticContentBucket.bucket, From 4bd3575bf00d08e2decf2b37c69882b58bf98bb7 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Wed, 11 Feb 2026 09:46:08 +0000 Subject: [PATCH 21/29] Nag suppression --- packages/cdk/stacks/nagSuppression.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/cdk/stacks/nagSuppression.ts b/packages/cdk/stacks/nagSuppression.ts index 9b7a460..fbf28ed 100644 --- a/packages/cdk/stacks/nagSuppression.ts +++ b/packages/cdk/stacks/nagSuppression.ts @@ -125,5 +125,25 @@ export const addNagSuppressions = (stack: Stack) => { ] ) + safeAddNagSuppression( + stack, + "/HackStack/ApiGateway/ApiGateway/Default/poll/GET/Resource", + [ + { + id: "AwsSolutions-APIG4", + reason: "this is for hack day stack - no auth is fine (for now)" + } + ] + ) + safeAddNagSuppression( + stack, + "/HackStack/ApiGateway/ApiGateway/Default/poll/GET/Resource", + [ + { + id: "AwsSolutions-COG4", + reason: "this is for hack day stack - no auth is fine" + } + ] + ) } From 50f3a7afe73348f76a136ef8589ce7e720378da7 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Wed, 11 Feb 2026 09:53:45 +0000 Subject: [PATCH 22/29] Add the execution polict for the poll lambda --- packages/cdk/stacks/HackStack.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cdk/stacks/HackStack.ts b/packages/cdk/stacks/HackStack.ts index e1e29ff..d3dfec5 100644 --- a/packages/cdk/stacks/HackStack.ts +++ b/packages/cdk/stacks/HackStack.ts @@ -116,7 +116,8 @@ export class HackStack extends Stack { const apiMethods = new RestApiGatewayMethods(this, "RestApiGatewayMethods", { executePolices: [ functions.fooLambda.executionPolicy, - functions.createLambda.executionPolicy + functions.createLambda.executionPolicy, + functions.pollLambda.executionPolicy ], restAPiGatewayRole: apiGateway.apiGatewayRole, restApiGateway: apiGateway.apiGateway, From 20726df607ee740a9804c89467e8cd4ab37f5627 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Wed, 11 Feb 2026 11:01:19 +0000 Subject: [PATCH 23/29] forward query strings to poll endpoint --- packages/cdk/resources/CloudfrontBehaviors.ts | 43 ++++++++++--------- packages/poll/src/handler.ts | 2 +- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/packages/cdk/resources/CloudfrontBehaviors.ts b/packages/cdk/resources/CloudfrontBehaviors.ts index 7286d45..5422273 100644 --- a/packages/cdk/resources/CloudfrontBehaviors.ts +++ b/packages/cdk/resources/CloudfrontBehaviors.ts @@ -31,7 +31,7 @@ export interface CloudfrontBehaviorsProps { * Any rewrites for cloudfront requests should go here */ -export class CloudfrontBehaviors extends Construct{ +export class CloudfrontBehaviors extends Construct { public readonly additionalBehaviors: Record public readonly s3404UriRewriteFunction: CloudfrontFunction public readonly s3404ModifyStatusCodeFunction: CloudfrontFunction @@ -40,31 +40,33 @@ export class CloudfrontBehaviors extends Construct{ public readonly keyValueStore: KeyValueStore public readonly fullCognitoDomain: string - public constructor(scope: Construct, id: string, props: CloudfrontBehaviorsProps){ + public constructor(scope: Construct, id: string, props: CloudfrontBehaviorsProps) { super(scope, id) // Resources const keyValueStore = new KeyValueStore(this, "FunctionsStore", { comment: `${props.serviceName}-KeyValueStore`, - source: ImportSource.fromInline(JSON.stringify({data: [ - { - key: "404_rewrite", - value: "404.html" - }, - { - key: "500_rewrite", - value: "500.html" - }, - { - key: "site_basePath", - value: "/site" - }, - { - key: "api_path", - value: "/api" - } - ]})) + source: ImportSource.fromInline(JSON.stringify({ + data: [ + { + key: "404_rewrite", + value: "404.html" + }, + { + key: "500_rewrite", + value: "500.html" + }, + { + key: "site_basePath", + value: "/site" + }, + { + key: "api_path", + value: "/api" + } + ] + })) }) // Workaround for CF KVS tag issues in latest cdk/cf, see: https://github.com/aws/aws-cdk/issues/36765 const cfnKeyValueStore = keyValueStore.node.defaultChild as CfnKeyValueStore @@ -165,6 +167,7 @@ export class CloudfrontBehaviors extends Construct{ allowedMethods: AllowedMethods.ALLOW_ALL, viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: CachePolicy.CACHING_DISABLED, + originRequestPolicy: props.apiGatewayRequestPolicy, functionAssociations: [ { function: apiGatewayStripPathFunction.function, diff --git a/packages/poll/src/handler.ts b/packages/poll/src/handler.ts index 19f7dfd..aac4814 100644 --- a/packages/poll/src/handler.ts +++ b/packages/poll/src/handler.ts @@ -26,7 +26,7 @@ const lambdaHandler = async (event: any): Promise => { logger.warn("No action ID provided in query parameters") return { statusCode: 400, - body: JSON.stringify({message: "Missing required query parameter: actionId"}), + body: JSON.stringify({message: "Missing required query parameter: actionid"}), headers: { "Content-Type": "application/json", "Cache-Control": "no-cache" From 1728430353eda9c811ed322cf0848a8c478a1fa9 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Wed, 11 Feb 2026 11:17:57 +0000 Subject: [PATCH 24/29] Refactor the dynamo bit --- packages/poll/src/dynamo.ts | 38 +++++++++--------------------------- packages/poll/src/handler.ts | 2 -- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/packages/poll/src/dynamo.ts b/packages/poll/src/dynamo.ts index 9e7a980..936cd1d 100644 --- a/packages/poll/src/dynamo.ts +++ b/packages/poll/src/dynamo.ts @@ -1,48 +1,28 @@ import {Logger} from "@aws-lambda-powertools/logger" -import {DynamoDBClient, QueryCommand, QueryCommandInput} from "@aws-sdk/client-dynamodb" +import {DynamoDBClient, GetItemCommand, GetItemCommandInput} from "@aws-sdk/client-dynamodb" const client = new DynamoDBClient() -const tableName = process.env.TABLE_NAME ?? "PrescriptionStatusUpdates" +const tableName = process.env.PROCESSING_STATUS_TABLE_NAME! export async function queryActionState( actionID: string, logger: Logger ): Promise> { - - // Use the GSI to query by PrescriptionID - const query: QueryCommandInput = { + const query: GetItemCommandInput = { TableName: tableName, - KeyConditionExpression: "actionId = :aid", - ExpressionAttributeValues: { - ":aid": {S: actionID} + Key: { + actionId: {S: actionID} } } - let lastEvaluatedKey - let items = [] - - logger.info("Querying DynamoDB for action ID", { + logger.info("Getting DynamoDB item for action ID", { actionID, tableName, }) try { - while (true) { - if (lastEvaluatedKey) { - query.ExclusiveStartKey = lastEvaluatedKey - } - - const result = await client.send(new QueryCommand(query)) - - if (result.Items) { - items.push(...result.Items) - } - - lastEvaluatedKey = result.LastEvaluatedKey - if (!lastEvaluatedKey) { - break - } - } + const result = await client.send(new GetItemCommand(query)) + const items = result.Item ? [result.Item] : [] logger.info("Retrieved records from DynamoDB", { actionID, @@ -51,7 +31,7 @@ export async function queryActionState( return items } catch (err) { - logger.error("Error querying DynamoDB for existing prescription records", { + logger.error("Error getting DynamoDB item for existing prescription records", { actionID, error: err }) diff --git a/packages/poll/src/handler.ts b/packages/poll/src/handler.ts index aac4814..9815bb6 100644 --- a/packages/poll/src/handler.ts +++ b/packages/poll/src/handler.ts @@ -8,8 +8,6 @@ import {queryActionState} from "./dynamo" const logger = new Logger({serviceName: "poll"}) -const tableName = process.env.PROCESSING_STATUS_TABLE_NAME! - const lambdaHandler = async (event: any): Promise => { logger.appendKeys({ "nhsd-correlation-id": event.headers["nhsd-correlation-id"], From 31d587c3ba7ef86d99afb7a2089356204d34bf8c Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Wed, 11 Feb 2026 11:29:53 +0000 Subject: [PATCH 25/29] Revert "forward query strings to poll endpoint" This reverts commit 20726df607ee740a9804c89467e8cd4ab37f5627. --- packages/cdk/resources/CloudfrontBehaviors.ts | 43 +++++++++---------- packages/poll/src/handler.ts | 2 +- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/packages/cdk/resources/CloudfrontBehaviors.ts b/packages/cdk/resources/CloudfrontBehaviors.ts index 5422273..7286d45 100644 --- a/packages/cdk/resources/CloudfrontBehaviors.ts +++ b/packages/cdk/resources/CloudfrontBehaviors.ts @@ -31,7 +31,7 @@ export interface CloudfrontBehaviorsProps { * Any rewrites for cloudfront requests should go here */ -export class CloudfrontBehaviors extends Construct { +export class CloudfrontBehaviors extends Construct{ public readonly additionalBehaviors: Record public readonly s3404UriRewriteFunction: CloudfrontFunction public readonly s3404ModifyStatusCodeFunction: CloudfrontFunction @@ -40,33 +40,31 @@ export class CloudfrontBehaviors extends Construct { public readonly keyValueStore: KeyValueStore public readonly fullCognitoDomain: string - public constructor(scope: Construct, id: string, props: CloudfrontBehaviorsProps) { + public constructor(scope: Construct, id: string, props: CloudfrontBehaviorsProps){ super(scope, id) // Resources const keyValueStore = new KeyValueStore(this, "FunctionsStore", { comment: `${props.serviceName}-KeyValueStore`, - source: ImportSource.fromInline(JSON.stringify({ - data: [ - { - key: "404_rewrite", - value: "404.html" - }, - { - key: "500_rewrite", - value: "500.html" - }, - { - key: "site_basePath", - value: "/site" - }, - { - key: "api_path", - value: "/api" - } - ] - })) + source: ImportSource.fromInline(JSON.stringify({data: [ + { + key: "404_rewrite", + value: "404.html" + }, + { + key: "500_rewrite", + value: "500.html" + }, + { + key: "site_basePath", + value: "/site" + }, + { + key: "api_path", + value: "/api" + } + ]})) }) // Workaround for CF KVS tag issues in latest cdk/cf, see: https://github.com/aws/aws-cdk/issues/36765 const cfnKeyValueStore = keyValueStore.node.defaultChild as CfnKeyValueStore @@ -167,7 +165,6 @@ export class CloudfrontBehaviors extends Construct { allowedMethods: AllowedMethods.ALLOW_ALL, viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: CachePolicy.CACHING_DISABLED, - originRequestPolicy: props.apiGatewayRequestPolicy, functionAssociations: [ { function: apiGatewayStripPathFunction.function, diff --git a/packages/poll/src/handler.ts b/packages/poll/src/handler.ts index 9815bb6..9035abc 100644 --- a/packages/poll/src/handler.ts +++ b/packages/poll/src/handler.ts @@ -24,7 +24,7 @@ const lambdaHandler = async (event: any): Promise => { logger.warn("No action ID provided in query parameters") return { statusCode: 400, - body: JSON.stringify({message: "Missing required query parameter: actionid"}), + body: JSON.stringify({message: "Missing required query parameter: actionId"}), headers: { "Content-Type": "application/json", "Cache-Control": "no-cache" From 93db56705f37f395202255160937e1be08f7114a Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Wed, 11 Feb 2026 11:35:38 +0000 Subject: [PATCH 26/29] Pass in json instead --- packages/poll/src/handler.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/poll/src/handler.ts b/packages/poll/src/handler.ts index 9035abc..496773f 100644 --- a/packages/poll/src/handler.ts +++ b/packages/poll/src/handler.ts @@ -17,14 +17,14 @@ const lambdaHandler = async (event: any): Promise => { "apigw-request-id": event.requestContext.requestId }) - // take the action ID from the query string parameters and use it to query the DynamoDB table for any existing records with that action ID - const actionID = event.queryStringParameters?.actionid + // take the action ID from the body and use it to query the DynamoDB table for any existing records with that action ID + const body = event.body ? JSON.parse(event.body) : null - if (!actionID) { - logger.warn("No action ID provided in query parameters") + if (!body || !body.actionid) { + logger.warn("No action ID provided in request body") return { statusCode: 400, - body: JSON.stringify({message: "Missing required query parameter: actionId"}), + body: JSON.stringify({message: "Missing required body parameter: actionid"}), headers: { "Content-Type": "application/json", "Cache-Control": "no-cache" @@ -32,6 +32,7 @@ const lambdaHandler = async (event: any): Promise => { } } + const actionID = body.actionid const result = await queryActionState(actionID, logger) return { From 561c09d2c182af5f5a5c97212c9ce12aaca3fc21 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Wed, 11 Feb 2026 12:38:56 +0000 Subject: [PATCH 27/29] Add request policy back in --- packages/cdk/resources/CloudfrontBehaviors.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cdk/resources/CloudfrontBehaviors.ts b/packages/cdk/resources/CloudfrontBehaviors.ts index 7286d45..7c3b889 100644 --- a/packages/cdk/resources/CloudfrontBehaviors.ts +++ b/packages/cdk/resources/CloudfrontBehaviors.ts @@ -165,6 +165,7 @@ export class CloudfrontBehaviors extends Construct{ allowedMethods: AllowedMethods.ALLOW_ALL, viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: CachePolicy.CACHING_DISABLED, + originRequestPolicy: props.apiGatewayRequestPolicy, functionAssociations: [ { function: apiGatewayStripPathFunction.function, From d6c53e5d50ae0f9a96a3f474c1a1ed45ca0fe3ee Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Wed, 11 Feb 2026 15:23:10 +0000 Subject: [PATCH 28/29] Needs to be post --- packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts b/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts index 479e3fc..1e0ae54 100644 --- a/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts +++ b/packages/cdk/resources/RestApiGateway/RestApiGatewayMethods.ts @@ -50,7 +50,7 @@ export class RestApiGatewayMethods extends Construct { }) const pollPrescriptionLambdaResource = props.restApiGateway.root.addResource("poll") - pollPrescriptionLambdaResource.addMethod("GET", new LambdaIntegration(props.pollLambda, { + pollPrescriptionLambdaResource.addMethod("POST", new LambdaIntegration(props.pollLambda, { credentialsRole: props.restAPiGatewayRole }), { authorizationType: AuthorizationType.NONE, From bcc1b57ecc6c4064f131cd04cc2d086f592da7d4 Mon Sep 17 00:00:00 2001 From: Jim Wild Date: Wed, 11 Feb 2026 15:31:24 +0000 Subject: [PATCH 29/29] Fix --- packages/cdk/resources/CloudfrontBehaviors.ts | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/cdk/resources/CloudfrontBehaviors.ts b/packages/cdk/resources/CloudfrontBehaviors.ts index 7c3b889..f725625 100644 --- a/packages/cdk/resources/CloudfrontBehaviors.ts +++ b/packages/cdk/resources/CloudfrontBehaviors.ts @@ -31,7 +31,7 @@ export interface CloudfrontBehaviorsProps { * Any rewrites for cloudfront requests should go here */ -export class CloudfrontBehaviors extends Construct{ +export class CloudfrontBehaviors extends Construct { public readonly additionalBehaviors: Record public readonly s3404UriRewriteFunction: CloudfrontFunction public readonly s3404ModifyStatusCodeFunction: CloudfrontFunction @@ -40,31 +40,33 @@ export class CloudfrontBehaviors extends Construct{ public readonly keyValueStore: KeyValueStore public readonly fullCognitoDomain: string - public constructor(scope: Construct, id: string, props: CloudfrontBehaviorsProps){ + public constructor(scope: Construct, id: string, props: CloudfrontBehaviorsProps) { super(scope, id) // Resources const keyValueStore = new KeyValueStore(this, "FunctionsStore", { comment: `${props.serviceName}-KeyValueStore`, - source: ImportSource.fromInline(JSON.stringify({data: [ - { - key: "404_rewrite", - value: "404.html" - }, - { - key: "500_rewrite", - value: "500.html" - }, - { - key: "site_basePath", - value: "/site" - }, - { - key: "api_path", - value: "/api" - } - ]})) + source: ImportSource.fromInline(JSON.stringify({ + data: [ + { + key: "404_rewrite", + value: "404.html" + }, + { + key: "500_rewrite", + value: "500.html" + }, + { + key: "site_basePath", + value: "/site" + }, + { + key: "api_path", + value: "/api" + } + ] + })) }) // Workaround for CF KVS tag issues in latest cdk/cf, see: https://github.com/aws/aws-cdk/issues/36765 const cfnKeyValueStore = keyValueStore.node.defaultChild as CfnKeyValueStore @@ -165,7 +167,6 @@ export class CloudfrontBehaviors extends Construct{ allowedMethods: AllowedMethods.ALLOW_ALL, viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: CachePolicy.CACHING_DISABLED, - originRequestPolicy: props.apiGatewayRequestPolicy, functionAssociations: [ { function: apiGatewayStripPathFunction.function,