Skip to content

Commit 26fbada

Browse files
committed
feat: add state machine construct inc. api gateway endpoint
1 parent 02b0af2 commit 26fbada

10 files changed

Lines changed: 713 additions & 31 deletions

File tree

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {IResource, PassthroughBehavior, StepFunctionsIntegration} from "aws-cdk-lib/aws-apigateway"
2+
import {IRole} from "aws-cdk-lib/aws-iam"
3+
import {HttpMethod} from "aws-cdk-lib/aws-lambda"
4+
import {Construct} from "constructs"
5+
import {stateMachineRequestTemplate} from "./templates/stateMachineRequest.js"
6+
import {stateMachine200ResponseTemplate, stateMachineErrorResponseTemplate} from "./templates/stateMachineResponses.js"
7+
import {ExpressStateMachine} from "../StateMachine.js"
8+
9+
export interface StateMachineEndpointProps {
10+
parentResource: IResource
11+
readonly resourceName: string
12+
readonly method: HttpMethod
13+
restApiGatewayRole: IRole
14+
stateMachine: ExpressStateMachine
15+
}
16+
17+
export class StateMachineEndpoint extends Construct {
18+
resource: IResource
19+
20+
public constructor(scope: Construct, id: string, props: StateMachineEndpointProps) {
21+
super(scope, id)
22+
23+
const requestTemplate = stateMachineRequestTemplate(props.stateMachine.stateMachine.stateMachineArn)
24+
25+
const resource = props.parentResource.addResource(props.resourceName)
26+
resource.addMethod(props.method, StepFunctionsIntegration.startExecution(props.stateMachine.stateMachine, {
27+
credentialsRole: props.restApiGatewayRole,
28+
passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH,
29+
requestTemplates: {
30+
"application/json": requestTemplate,
31+
"application/fhir+json": requestTemplate
32+
},
33+
integrationResponses: [
34+
{
35+
statusCode: "200",
36+
responseTemplates: {
37+
"application/json": stateMachine200ResponseTemplate
38+
}
39+
},
40+
{
41+
statusCode: "400",
42+
selectionPattern: "^4\\d{2}.*",
43+
responseTemplates: {
44+
"application/json": stateMachineErrorResponseTemplate("400")
45+
}
46+
},
47+
{
48+
statusCode: "500",
49+
selectionPattern: "^5\\d{2}.*",
50+
responseTemplates: {
51+
"application/json": stateMachineErrorResponseTemplate("500")
52+
}
53+
}
54+
]
55+
}), {
56+
methodResponses: []
57+
})
58+
59+
this.resource = resource
60+
}
61+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* eslint-disable max-len */
2+
export const stateMachineRequestTemplate = (stateMachineArn: string) => {
3+
return `## Velocity Template used for API Gateway request mapping template
4+
## "@@" is used here as a placeholder for '"' to avoid using escape characters.
5+
6+
#set($includeHeaders = true)
7+
#set($includeQueryString = true)
8+
#set($includePath = true)
9+
#set($requestContext = '')
10+
11+
#set($inputString = '')
12+
#set($allParams = $input.params())
13+
#set($allParams.header.apigw-request-id = $context.requestId)
14+
{
15+
"stateMachineArn": "${stateMachineArn}",
16+
#set($inputString = "$inputString,@@body@@: $input.body")
17+
#if ($includeHeaders)
18+
#set($inputString = "$inputString, @@headers@@:{")
19+
#foreach($paramName in $allParams.header.keySet())
20+
#set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@")
21+
#if($foreach.hasNext)
22+
#set($inputString = "$inputString,")
23+
#end
24+
#end
25+
#set($inputString = "$inputString }")
26+
#end
27+
#if ($includeQueryString)
28+
#set($inputString = "$inputString, @@queryStringParameters@@:{")
29+
#foreach($paramName in $allParams.querystring.keySet())
30+
#set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@")
31+
#if($foreach.hasNext)
32+
#set($inputString = "$inputString,")
33+
#end
34+
#end
35+
#set($inputString = "$inputString }")
36+
#end
37+
#if ($includePath)
38+
#set($inputString = "$inputString, @@pathParameters@@:{")
39+
#foreach($paramName in $allParams.path.keySet())
40+
#set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@")
41+
#if($foreach.hasNext)
42+
#set($inputString = "$inputString,")
43+
#end
44+
#end
45+
#set($inputString = "$inputString }")
46+
#end
47+
## Check if the request context should be included as part of the execution input
48+
#if($requestContext && !$requestContext.empty)
49+
#set($inputString = "$inputString,")
50+
#set($inputString = "$inputString @@requestContext@@: $requestContext")
51+
#end
52+
#set($inputString = "$inputString}")
53+
#set($inputString = $inputString.replaceAll("@@",'"'))
54+
#set($len = $inputString.length() - 1)
55+
"input": "{$util.escapeJavaScript($inputString.substring(1,$len))}"
56+
}`
57+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/* eslint-disable max-len */
2+
export const stateMachine200ResponseTemplate = `#set($payload = $util.parseJson($input.path('$.output')))
3+
#set($context.responseOverride.status = $payload.Payload.statusCode)
4+
#set($allHeaders = $payload.Payload.headers)
5+
#foreach($headerName in $allHeaders.keySet())
6+
#set($context.responseOverride.header[$headerName] = $allHeaders.get($headerName))
7+
#end
8+
$payload.Payload.body`
9+
10+
interface ErrorMap {
11+
[key: string]: {
12+
code: string
13+
severity: string
14+
diagnostics: string
15+
codingCode: string
16+
codingDisplay: string
17+
}
18+
}
19+
20+
const getOperationOutcome = (status: string) => {
21+
const errorMap: ErrorMap = {
22+
400: {
23+
code: "value",
24+
severity: "error",
25+
diagnostics: "Invalid request.",
26+
codingCode: "BAD_REQUEST",
27+
codingDisplay: "400: The Server was unable to process the request"
28+
},
29+
500: {
30+
code: "exception",
31+
severity: "fatal",
32+
diagnostics: "Unknown Error.",
33+
codingCode: "SERVER_ERROR",
34+
codingDisplay: "500: The Server has encountered an error processing the request."
35+
}
36+
}
37+
38+
return JSON.stringify({
39+
ResourceType: "OperationOutcome",
40+
issue: [
41+
{
42+
code: errorMap[status].code,
43+
severity: errorMap[status].severity,
44+
diagnostics: errorMap[status],
45+
details: {
46+
coding: [
47+
{
48+
system: "https://fhir.nhs.uk/CodeSystem/http-error-codes",
49+
code: errorMap[status].codingCode,
50+
display: errorMap[status].codingDisplay
51+
}
52+
]
53+
}
54+
}
55+
]
56+
})
57+
}
58+
59+
export const stateMachineErrorResponseTemplate = (status: string) => `#set($context.responseOverride.header["Content-Type"] ="application/fhir+json")
60+
${getOperationOutcome(status)}`

0 commit comments

Comments
 (0)