Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
02b0af2
refactor: api gateway from cpt for reuse
tstephen-nhs Mar 18, 2026
455b657
Merge branch 'main' into aea-6254-cdk-api-gateway
tstephen-nhs Mar 18, 2026
d3594ca
Merge branch 'main' into aea-6254-cdk-api-gateway
tstephen-nhs Mar 24, 2026
bb83c29
fix: enforce expected role
tstephen-nhs Mar 25, 2026
74d0de2
fix: make LogGroup child of API gateway
tstephen-nhs Mar 25, 2026
c7d714d
chore: add sonarqube plugin for 'clean as you code'
tstephen-nhs Mar 25, 2026
f2e9325
fix: protect against enabling csoc with no destination
tstephen-nhs Mar 25, 2026
0ac46b1
chore: clean imports
tstephen-nhs Mar 25, 2026
b500519
chore: ignore SQ rather than add extra var declaration
tstephen-nhs Mar 25, 2026
4faca4a
feat: add state machine construct inc. api gateway endpoint
tstephen-nhs Mar 18, 2026
f701083
chore: fix 'any' use
tstephen-nhs Mar 18, 2026
27484f9
docs: JS doc
tstephen-nhs Mar 26, 2026
1346790
docs: jsdoc, plus String.raw for regex
tstephen-nhs Mar 26, 2026
ccb5129
docs: example usage of StateMachine
tstephen-nhs Mar 26, 2026
f4d4506
docs: example of ApiGateway use
tstephen-nhs Mar 26, 2026
e35ce32
refactor: centralise constants
tstephen-nhs Mar 26, 2026
c734151
refactor: centralise constants
tstephen-nhs Mar 26, 2026
d464766
Upgrade: [dependabot] - bump NHSDigital/eps-common-workflows/.github/…
dependabot[bot] Mar 26, 2026
831746b
Upgrade: [dependabot] - bump NHSDigital/eps-common-workflows/.github/…
dependabot[bot] Mar 26, 2026
90c37b7
Upgrade: [dependabot] - bump NHSDigital/eps-common-workflows/.github/…
dependabot[bot] Mar 26, 2026
4f7415c
Upgrade: [dependabot] - bump NHSDigital/eps-common-workflows/.github/…
dependabot[bot] Mar 26, 2026
9048e4f
Upgrade: [dependabot] - bump NHSDigital/eps-common-workflows/.github/…
dependabot[bot] Mar 26, 2026
cd35a79
Upgrade: [dependabot] - bump constructs from 10.5.1 to 10.6.0 (#626)
dependabot[bot] Mar 26, 2026
76a062b
Upgrade: [dependabot] - bump @vitest/coverage-v8 from 4.1.0 to 4.1.2 …
dependabot[bot] Mar 26, 2026
f231ac7
Upgrade: [dependabot] - bump eslint from 10.0.3 to 10.1.0 (#632)
dependabot[bot] Mar 26, 2026
0874ea7
Upgrade: [dependabot] - bump @aws-sdk/client-s3 from 3.1013.0 to 3.10…
dependabot[bot] Mar 26, 2026
8fe08e2
Upgrade: [dependabot] - bump picomatch from 2.3.1 to 2.3.2 (#638)
dependabot[bot] Mar 26, 2026
fa0ae51
Upgrade: [dependabot] - bump aws-cdk from 2.1112.0 to 2.1114.1 (#636)
dependabot[bot] Mar 26, 2026
37bdda4
Upgrade: [dependabot] - bump @aws-sdk/client-lambda from 3.1013.0 to …
dependabot[bot] Mar 26, 2026
6085fe1
Upgrade: [dependabot] - bump @typescript-eslint/eslint-plugin from 8.…
dependabot[bot] Mar 26, 2026
72bb3f9
Upgrade: [dependabot] - bump @aws-sdk/client-cloudformation from 3.10…
dependabot[bot] Mar 26, 2026
5080ce5
Upgrade: [dependabot] - bump @aws-sdk/client-route-53 from 3.1013.0 t…
dependabot[bot] Mar 26, 2026
602b2f0
Upgrade: [dependabot] - bump handlebars from 4.7.8 to 4.7.9 (#639)
dependabot[bot] Mar 27, 2026
dc74bf6
revert git secrets install
tstephen-nhs Mar 27, 2026
4ccfaf1
fix: use postCreate to avoid git-secrets failing on second and subseq…
tstephen-nhs Mar 27, 2026
c5d9ba9
docs: add copilot instructions and teach it to write JSDoc
tstephen-nhs Mar 27, 2026
af7a124
docs: rereview jsdoc in light of new copilot instructions
tstephen-nhs Mar 27, 2026
6107867
Merge branch 'main' into aea-6256-cdk-statemachine
tstephen-nhs Mar 27, 2026
23f64d3
chore: placate SQ
tstephen-nhs Mar 27, 2026
525fdcc
Merge branch 'main' into aea-6256-cdk-statemachine
tstephen-nhs Mar 27, 2026
8956b70
chore: npm audit fix
tstephen-nhs Mar 27, 2026
8ca91e6
fix: typo on cache strategy
tstephen-nhs Mar 27, 2026
fb774c5
fix: typo in const name
tstephen-nhs Mar 27, 2026
9fd0795
fix: diagnostics in error response
tstephen-nhs Mar 27, 2026
9d2e21f
fix: copy paste naming error
tstephen-nhs Mar 27, 2026
0a2a573
fix: typo on log stream resource
tstephen-nhs Mar 27, 2026
464dc01
fix: enumerate potential status codes
tstephen-nhs Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {IResource, PassthroughBehavior, StepFunctionsIntegration} from "aws-cdk-lib/aws-apigateway"
import {IRole} from "aws-cdk-lib/aws-iam"
import {HttpMethod} from "aws-cdk-lib/aws-lambda"
import {Construct} from "constructs"
import {stateMachineRequestTemplate} from "./templates/stateMachineRequest.js"
import {stateMachine200ResponseTemplate, stateMachineErrorResponseTemplate} from "./templates/stateMachineResponses.js"
import {ExpressStateMachine} from "../StateMachine.js"

/** Parameters used to create an API endpoint backed by a Step Functions Express workflow. */
export interface StateMachineEndpointProps {
/** Parent API resource under which the state machine endpoint is added. */
parentResource: IResource
/** Path segment used to create the child API resource. */
readonly resourceName: string
/** HTTP verb bound to the Step Functions integration. */
readonly method: HttpMethod
/** Invocation role used by API Gateway when starting workflow executions. */
restApiGatewayRole: IRole
/** State machine wrapper construct providing the target workflow ARN and integration target. */
stateMachine: ExpressStateMachine
}

/** Adds an API Gateway resource/method that starts an Express Step Functions execution. */
export class StateMachineEndpoint extends Construct {
/** API resource created by this construct. */
resource: IResource

/** Wires request and response mapping templates for JSON and FHIR payload flows. */
public constructor(scope: Construct, id: string, props: StateMachineEndpointProps) {
super(scope, id)

const requestTemplate = stateMachineRequestTemplate(props.stateMachine.stateMachine.stateMachineArn)

const resource = props.parentResource.addResource(props.resourceName)
resource.addMethod(props.method, StepFunctionsIntegration.startExecution(props.stateMachine.stateMachine, {
credentialsRole: props.restApiGatewayRole,
passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH,
requestTemplates: {
"application/json": requestTemplate,
"application/fhir+json": requestTemplate
},
integrationResponses: [
{
statusCode: "200",
responseTemplates: {
"application/json": stateMachine200ResponseTemplate
}
},
{
statusCode: "400",
selectionPattern: String.raw`^4\d{2}.*`,
responseTemplates: {
"application/json": stateMachineErrorResponseTemplate("400")
}
},
{
statusCode: "500",
selectionPattern: String.raw`^5\d{2}.*`,
responseTemplates: {
"application/json": stateMachineErrorResponseTemplate("500")
}
}
]
}), {
methodResponses: [
{ statusCode: "200" },
{ statusCode: "400" },
{ statusCode: "500" }
]
})

this.resource = resource
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* eslint-disable max-len */
/**
* @returns API Gateway request mapping template for StartExecution payloads.
*/
export const stateMachineRequestTemplate = (stateMachineArn: string) => {
return `## Velocity Template used for API Gateway request mapping template
## "@@" is used here as a placeholder for '"' to avoid using escape characters.

#set($includeHeaders = true)
#set($includeQueryString = true)
#set($includePath = true)
#set($requestContext = '')

#set($inputString = '')
#set($allParams = $input.params())
#set($allParams.header.apigw-request-id = $context.requestId)
{
"stateMachineArn": "${stateMachineArn}",
#set($inputString = "$inputString,@@body@@: $input.body")
#if ($includeHeaders)
#set($inputString = "$inputString, @@headers@@:{")
#foreach($paramName in $allParams.header.keySet())
#set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@")
#if($foreach.hasNext)
#set($inputString = "$inputString,")
#end
#end
#set($inputString = "$inputString }")
#end
#if ($includeQueryString)
#set($inputString = "$inputString, @@queryStringParameters@@:{")
#foreach($paramName in $allParams.querystring.keySet())
#set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@")
#if($foreach.hasNext)
#set($inputString = "$inputString,")
#end
#end
#set($inputString = "$inputString }")
#end
#if ($includePath)
#set($inputString = "$inputString, @@pathParameters@@:{")
#foreach($paramName in $allParams.path.keySet())
#set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@")
#if($foreach.hasNext)
#set($inputString = "$inputString,")
#end
#end
#set($inputString = "$inputString }")
#end
## Check if the request context should be included as part of the execution input
#if($requestContext && !$requestContext.empty)
#set($inputString = "$inputString,")
#set($inputString = "$inputString @@requestContext@@: $requestContext")
#end
#set($inputString = "$inputString}")
#set($inputString = $inputString.replaceAll("@@",'"'))
#set($len = $inputString.length() - 1)
"input": "{$util.escapeJavaScript($inputString.substring(1,$len))}"
}`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* eslint-disable max-len */
/** VTL response template that unwraps successful workflow output and forwards status and headers. */
export const stateMachine200ResponseTemplate = `#set($payload = $util.parseJson($input.path('$.output')))
#set($context.responseOverride.status = $payload.Payload.statusCode)
#set($allHeaders = $payload.Payload.headers)
#foreach($headerName in $allHeaders.keySet())
#set($context.responseOverride.header[$headerName] = $allHeaders.get($headerName))
#end
$payload.Payload.body`

interface ErrorMap {
[key: string]: {
code: string
severity: string
diagnostics: string
codingCode: string
codingDisplay: string
}
}

const getOperationOutcome = (status: string) => {
const errorMap: ErrorMap = {
400: {
code: "value",
severity: "error",
diagnostics: "Invalid request.",
codingCode: "BAD_REQUEST",
codingDisplay: "400: The Server was unable to process the request"
},
500: {
code: "exception",
severity: "fatal",
diagnostics: "Unknown Error.",
codingCode: "SERVER_ERROR",
codingDisplay: "500: The Server has encountered an error processing the request."
}
}

return JSON.stringify({
ResourceType: "OperationOutcome",
issue: [
{
code: errorMap[status].code,
severity: errorMap[status].severity,
diagnostics: errorMap[status].diagnostics,
details: {
coding: [
{
system: "https://fhir.nhs.uk/CodeSystem/http-error-codes",
code: errorMap[status].codingCode,
display: errorMap[status].codingDisplay
Comment thread
tstephen-nhs marked this conversation as resolved.
}
]
}
}
]
})
}

/**
* @returns VTL response template that maps workflow failures to FHIR OperationOutcome payloads.
*/
export const stateMachineErrorResponseTemplate = (status: string) => `#set($context.responseOverride.header["Content-Type"] ="application/fhir+json")
${getOperationOutcome(status)}`
Loading
Loading