├── jest ├── setupAfterEnv.ts └── jest.config.js ├── .npmignore ├── .gitignore ├── functions └── background-check.ts ├── lib ├── cfn-output.ts ├── __tests__ │ ├── branching-logic-e2e.mocks.json │ ├── simplistic-e2e.test.ts │ ├── pass-states-integration.test.ts │ └── branching-logic-e2e.test.ts ├── sfn-testing-stack.ts ├── pass-states-integration.ts ├── extract-env.ts ├── simplistic-e2e.ts └── branching-logic-e2e.ts ├── README.md ├── tsconfig.json ├── bin └── sfn-testing.ts ├── cdk.json └── package.json /jest/setupAfterEnv.ts: -------------------------------------------------------------------------------- 1 | import "aws-testing-library/lib/jest"; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | .swc 4 | .env 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | .env 6 | 7 | # CDK asset staging directory 8 | .cdk.staging 9 | cdk.out 10 | -------------------------------------------------------------------------------- /functions/background-check.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A sample function that might throw an error that we would like to retry. 3 | */ 4 | const handler = async () => { 5 | return { 6 | status: "PASS" 7 | }; 8 | }; 9 | 10 | export { handler }; 11 | -------------------------------------------------------------------------------- /jest/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: "node", 3 | testMatch: ["**/*.test.ts"], 4 | rootDir: "../", 5 | transform: { 6 | "^.+\\.(t|j)sx?$": "@swc-node/jest" 7 | }, 8 | setupFiles: ["dotenv/config"], 9 | setupFilesAfterEnv: ["/jest/setupAfterEnv.ts"], 10 | transformIgnorePatterns: ["node_modules/(?!(filter-obj)/)"] 11 | }; 12 | -------------------------------------------------------------------------------- /lib/cfn-output.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | export class CfnOutput extends cdk.CfnOutput { 5 | constructor(scope: Construct, id: string, props: cdk.CfnOutputProps) { 6 | super(scope, id, props); 7 | 8 | /** 9 | * Ensures that the output name is not prefixed with a random identifier. 10 | */ 11 | this.overrideLogicalId(id); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/__tests__/branching-logic-e2e.mocks.json: -------------------------------------------------------------------------------- 1 | { 2 | "StateMachines": { 3 | "BranchingLogic": { 4 | "TestCases": { 5 | "ErrorPath": { 6 | "BackgroundCheckStep": "BackgroundCheckError" 7 | } 8 | } 9 | } 10 | }, 11 | "MockedResponses": { 12 | "BackgroundCheckError": { 13 | "0": { 14 | "Throw": { 15 | "Error": "Lambda.TimeoutException", 16 | "Cause": "Lambda timed out." 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Testing AWS Step Functions flows 2 | 3 | This repository contains examples of how one might test various AWS Step Functions flows. 4 | 5 | The test files are located in the `lib/__tests__` directory. 6 | 7 | ## Deployment 8 | 9 | 1. `npm run boostrap` 10 | 2. `npm run deploy` 11 | 12 | ## Running the tests 13 | 14 | 1. Ensure that Docker is running. 15 | 16 | 2. Pull the [_aws-stepfunctions-local_](https://docs.aws.amazon.com/step-functions/latest/dg/sfn-local-docker.html) image. 17 | 18 | 3. `npm run test` 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /bin/sfn-testing.ts: -------------------------------------------------------------------------------- 1 | import "source-map-support/register"; 2 | import * as cdk from "aws-cdk-lib"; 3 | import { SFNTestingStack } from "../lib/sfn-testing-stack"; 4 | import { IAspect } from "aws-cdk-lib"; 5 | import { IConstruct } from "constructs"; 6 | 7 | const app = new cdk.App(); 8 | 9 | const stack = new SFNTestingStack(app, "SFNTestingStack", { 10 | synthesizer: new cdk.DefaultStackSynthesizer({ 11 | qualifier: "sfntesting" 12 | }) 13 | }); 14 | 15 | class DestroyAll implements IAspect { 16 | public visit(node: IConstruct): void { 17 | if (node instanceof cdk.CfnResource) { 18 | node.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); 19 | } 20 | } 21 | } 22 | 23 | cdk.Aspects.of(stack).add(new DestroyAll()); 24 | -------------------------------------------------------------------------------- /lib/sfn-testing-stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { BranchingLogicE2E } from "./branching-logic-e2e"; 4 | import { CfnOutput } from "./cfn-output"; 5 | import { PassStatesIntegration } from "./pass-states-integration"; 6 | import { SimplisticE2E } from "./simplistic-e2e"; 7 | 8 | export class SFNTestingStack extends Stack { 9 | constructor(scope: Construct, id: string, props?: StackProps) { 10 | super(scope, id, props); 11 | 12 | new CfnOutput(this, "AwsRegion", { 13 | value: this.region 14 | }); 15 | 16 | new SimplisticE2E(this, "SimplisticE2E"); 17 | new BranchingLogicE2E(this, "BranchingLogicE2E"); 18 | new PassStatesIntegration(this, "PassStatesIntegration"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/__tests__/simplistic-e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { SFNClient, StartExecutionCommand } from "@aws-sdk/client-sfn"; 2 | 3 | const sfnClient = new SFNClient({}); 4 | 5 | test("Saves the user in the DynamoDB table", async () => { 6 | const startExecutionResult = await sfnClient.send( 7 | new StartExecutionCommand({ 8 | stateMachineArn: process.env.SIMPLISTIC_E2E_STEP_FUNCTION_ARN, 9 | input: JSON.stringify({ 10 | firstName: "John", 11 | lastName: "Doe" 12 | }) 13 | }) 14 | ); 15 | 16 | await expect({ 17 | region: process.env.AWS_REGION, 18 | table: process.env.SIMPLISTIC_E2E_DATA_TABLE_NAME 19 | }).toHaveItem( 20 | { 21 | PK: `USER#${startExecutionResult.executionArn}` 22 | }, 23 | { 24 | PK: `USER#${startExecutionResult.executionArn}`, 25 | firstName: "John", 26 | lastName: "Doe" 27 | } 28 | ); 29 | }, 15_000); 30 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "node -r @swc-node/register ./bin/sfn-testing.ts", 3 | "watch": { 4 | "include": ["**"], 5 | "exclude": [ 6 | "README.md", 7 | "cdk*.json", 8 | "**/*.d.ts", 9 | "**/*.js", 10 | "tsconfig.json", 11 | "package*.json", 12 | "yarn.lock", 13 | "node_modules", 14 | "test" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 23 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 24 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 25 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/pass-states-integration.ts: -------------------------------------------------------------------------------- 1 | import { aws_stepfunctions } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | 4 | export class PassStatesIntegration extends Construct { 5 | public transformDataStep: aws_stepfunctions.Pass; 6 | 7 | constructor(scope: Construct, id: string) { 8 | super(scope, id); 9 | 10 | this.transformDataStep = new aws_stepfunctions.Pass( 11 | this, 12 | "TransformDataStep", 13 | { 14 | parameters: { 15 | payload: aws_stepfunctions.JsonPath.stringAt( 16 | "States.Format('{} {}', $.firstName, $.lastName)" 17 | ) 18 | } 19 | } 20 | ); 21 | 22 | // You can imagine the definition being a bit more complex. 23 | const stepFunctionDefinition = this.transformDataStep; 24 | 25 | const stepFunction = new aws_stepfunctions.StateMachine( 26 | this, 27 | "StepFunction", 28 | { 29 | definition: stepFunctionDefinition 30 | } 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code", 3 | "version": "0.1.0", 4 | "bin": { 5 | "code": "bin/code.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest --config jest/jest.config.js", 11 | "cdk": "cdk", 12 | "bootstrap": "npm run cdk bootstrap -- --qualifier=sfntesting --toolkit-stack-name SFNTestingBootstrap", 13 | "deploy": "npm run cdk deploy -- --hotswap && npm run env", 14 | "env": "node -r @swc-node/register ./lib/extract-env.ts SFNTestingStack" 15 | }, 16 | "devDependencies": { 17 | "@aws-sdk/client-cloudformation": "3.51.0", 18 | "@aws-sdk/client-sfn": "3.51.0", 19 | "@swc-node/jest": "1.4.3", 20 | "@swc-node/register": "1.4.2", 21 | "@types/jest": "27.4.0", 22 | "@types/node": "10.17.27", 23 | "aws-cdk": "2.12.0", 24 | "aws-cdk-lib": "2.12.0", 25 | "aws-testing-library": "2.1.1", 26 | "change-case": "4.1.2", 27 | "constructs": "10.0.61", 28 | "dotenv": "16.0.0", 29 | "esbuild": "0.14.21", 30 | "jest": "27.5.1", 31 | "prettier": "2.5.1", 32 | "source-map-support": "0.5.21", 33 | "testcontainers": "8.2.0", 34 | "typescript": "4.5.5", 35 | "wait-for-expect": "3.0.2" 36 | }, 37 | "dependencies": {} 38 | } 39 | -------------------------------------------------------------------------------- /lib/extract-env.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from "fs"; 2 | import { join } from "path"; 3 | import { constantCase } from "change-case"; 4 | import * as prettier from "prettier"; 5 | import { 6 | CloudFormationClient, 7 | DescribeStacksCommand, 8 | Output 9 | } from "@aws-sdk/client-cloudformation"; 10 | 11 | const cfnClient = new CloudFormationClient({}); 12 | 13 | async function main() { 14 | const stackName = process.argv[2]; 15 | if (!stackName) { 16 | throw new Error("Stack name is required"); 17 | } 18 | 19 | const stackOutputs = await cfnClient.send( 20 | new DescribeStacksCommand({ 21 | StackName: stackName 22 | }) 23 | ); 24 | 25 | const rootPath = join(__dirname, "../"); 26 | const outputs = stackOutputs.Stacks?.[0].Outputs ?? []; 27 | 28 | await Promise.all([ 29 | createEnvFile(outputs, rootPath), 30 | createTypingsFile(outputs, rootPath) 31 | ]); 32 | } 33 | 34 | main(); 35 | 36 | async function createEnvFile(stackOutputs: Output[], rootPath: string) { 37 | const envFileContents = stackOutputs.reduce((envFileContents, output) => { 38 | const envOutputKey = constantCase(output.OutputKey as string); 39 | const envOutputValue = output.OutputValue as string; 40 | 41 | envFileContents += `${envOutputKey}=${envOutputValue}\n`; 42 | return envFileContents; 43 | }, ""); 44 | 45 | const envFilePath = join(rootPath, ".env"); 46 | writeFileSync(envFilePath, envFileContents); 47 | } 48 | 49 | async function createTypingsFile(stackOutputs: Output[], rootPath: string) { 50 | const typingsFileContents = stackOutputs.reduce( 51 | (typingsFileContents, output) => { 52 | const envOutputKey = constantCase(output.OutputKey as string); 53 | const envOutputValue = output.OutputValue as string; 54 | 55 | typingsFileContents += `${envOutputKey}:"${envOutputValue}";\n`; 56 | return typingsFileContents; 57 | }, 58 | "" 59 | ); 60 | 61 | const typingsFile = prettier.format( 62 | ` 63 | declare namespace NodeJS { 64 | export interface ProcessEnv { 65 | ${typingsFileContents} 66 | } 67 | } 68 | `, 69 | { parser: "typescript" } 70 | ); 71 | const typingsFilePath = join(rootPath, "env.d.ts"); 72 | writeFileSync(typingsFilePath, typingsFile); 73 | } 74 | -------------------------------------------------------------------------------- /lib/simplistic-e2e.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_dynamodb, 3 | aws_stepfunctions, 4 | aws_stepfunctions_tasks 5 | } from "aws-cdk-lib"; 6 | import { Construct } from "constructs"; 7 | import { CfnOutput } from "./cfn-output"; 8 | 9 | export class SimplisticE2E extends Construct { 10 | constructor(scope: Construct, id: string) { 11 | super(scope, id); 12 | 13 | const dataTable = new aws_dynamodb.Table(this, "DataTable", { 14 | partitionKey: { 15 | name: "PK", 16 | type: aws_dynamodb.AttributeType.STRING 17 | }, 18 | billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST 19 | }); 20 | new CfnOutput(this, "SimplisticE2eDataTableName", { 21 | value: dataTable.tableName 22 | }); 23 | 24 | const transformDataStep = new aws_stepfunctions.Pass( 25 | this, 26 | "TransformData", 27 | { 28 | parameters: { 29 | firstName: aws_stepfunctions.JsonPath.stringAt("$.firstName"), 30 | lastName: aws_stepfunctions.JsonPath.stringAt("$.lastName"), 31 | PK: aws_stepfunctions.JsonPath.stringAt( 32 | "States.Format('USER#{}', $$.Execution.Id)" 33 | ) 34 | } 35 | } 36 | ); 37 | 38 | const saveUserStep = new aws_stepfunctions_tasks.DynamoPutItem( 39 | this, 40 | "SaveTheUser", 41 | { 42 | item: { 43 | firstName: aws_stepfunctions_tasks.DynamoAttributeValue.fromString( 44 | aws_stepfunctions.JsonPath.stringAt("$.firstName") 45 | ), 46 | lastName: aws_stepfunctions_tasks.DynamoAttributeValue.fromString( 47 | aws_stepfunctions.JsonPath.stringAt("$.lastName") 48 | ), 49 | PK: aws_stepfunctions_tasks.DynamoAttributeValue.fromString( 50 | aws_stepfunctions.JsonPath.stringAt("$.PK") 51 | ) 52 | }, 53 | table: dataTable 54 | } 55 | ); 56 | 57 | const stepFunctionDefinition = transformDataStep.next(saveUserStep); 58 | 59 | const stepFunction = new aws_stepfunctions.StateMachine( 60 | this, 61 | "StepFunction", 62 | { 63 | definition: stepFunctionDefinition 64 | } 65 | ); 66 | new CfnOutput(this, "SimplisticE2eStepFunctionArn", { 67 | value: stepFunction.stateMachineArn 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/__tests__/pass-states-integration.test.ts: -------------------------------------------------------------------------------- 1 | import { GenericContainer, StartedTestContainer } from "testcontainers"; 2 | import { PassStatesIntegration } from "../pass-states-integration"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import { 5 | CreateStateMachineCommand, 6 | GetExecutionHistoryCommand, 7 | SFNClient, 8 | StartExecutionCommand 9 | } from "@aws-sdk/client-sfn"; 10 | import waitFor from "wait-for-expect"; 11 | 12 | let container: StartedTestContainer | undefined; 13 | 14 | beforeAll(async () => { 15 | container = await new GenericContainer("amazon/aws-stepfunctions-local") 16 | .withExposedPorts(8083) 17 | .withEnv("AWS_DEFAULT_REGION", process.env.AWS_REGION) 18 | .start(); 19 | }, 15_000); 20 | 21 | afterAll(async () => { 22 | await container?.stop(); 23 | }, 15_000); 24 | 25 | test("Handles the failure of the BackgroundCheck step", async () => { 26 | const stack = new cdk.Stack(); 27 | const construct = new PassStatesIntegration(stack, "PassStatesIntegration"); 28 | 29 | const transformDataStepDefinition = construct.transformDataStep.toStateJson(); 30 | const stepFunctionDefinition = JSON.stringify({ 31 | StartAt: "TransformDataStep", 32 | States: { 33 | TransformDataStep: { 34 | ...transformDataStepDefinition, 35 | End: true 36 | } 37 | } 38 | }); 39 | 40 | const sfnLocalClient = new SFNClient({ 41 | endpoint: `http://localhost:${container?.getMappedPort(8083)}` 42 | }); 43 | 44 | const createLocalSFNResult = await sfnLocalClient.send( 45 | new CreateStateMachineCommand({ 46 | definition: stepFunctionDefinition, 47 | name: "PassStates", 48 | roleArn: "arn:aws:iam::012345678901:role/DummyRole" 49 | }) 50 | ); 51 | 52 | const startLocalSFNExecutionResult = await sfnLocalClient.send( 53 | new StartExecutionCommand({ 54 | stateMachineArn: createLocalSFNResult.stateMachineArn, 55 | input: JSON.stringify({ 56 | firstName: "John", 57 | lastName: "Doe" 58 | }) 59 | }) 60 | ); 61 | 62 | await waitFor(async () => { 63 | const getExecutionHistoryResult = await sfnLocalClient.send( 64 | new GetExecutionHistoryCommand({ 65 | executionArn: startLocalSFNExecutionResult.executionArn 66 | }) 67 | ); 68 | 69 | const successState = getExecutionHistoryResult.events?.find( 70 | event => event.type == "ExecutionSucceeded" 71 | ); 72 | 73 | expect(successState?.executionSucceededEventDetails?.output).toEqual( 74 | JSON.stringify({ payload: "John Doe" }) 75 | ); 76 | }); 77 | }, 20_000); 78 | -------------------------------------------------------------------------------- /lib/__tests__/branching-logic-e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateStateMachineCommand, 3 | DescribeStateMachineCommand, 4 | SFNClient, 5 | StartExecutionCommand 6 | } from "@aws-sdk/client-sfn"; 7 | import { join } from "path"; 8 | import { GenericContainer, StartedTestContainer } from "testcontainers"; 9 | 10 | let container: StartedTestContainer | undefined; 11 | 12 | beforeAll(async () => { 13 | const mockConfigPath = join(__dirname, "./branching-logic-e2e.mocks.json"); 14 | container = await new GenericContainer("amazon/aws-stepfunctions-local") 15 | .withExposedPorts(8083) 16 | .withBindMount(mockConfigPath, "/home/branching-logic-e2e.mocks.json", "ro") 17 | .withEnv("SFN_MOCK_CONFIG", "/home/branching-logic-e2e.mocks.json") 18 | .withEnv("AWS_ACCESS_KEY_ID", process.env.AWS_ACCESS_KEY_ID as string) 19 | .withEnv( 20 | "AWS_SECRET_ACCESS_KEY", 21 | process.env.AWS_SECRET_ACCESS_KEY as string 22 | ) 23 | /** 24 | * For federated credentials (for example, SSO), this environment variable is required. 25 | */ 26 | .withEnv("AWS_SESSION_TOKEN", process.env.AWS_SESSION_TOKEN as string) 27 | .withEnv("AWS_DEFAULT_REGION", process.env.AWS_REGION) 28 | .start(); 29 | }, 15_000); 30 | 31 | afterAll(async () => { 32 | await container?.stop(); 33 | }, 15_000); 34 | 35 | test("Handles the failure of the BackgroundCheck step", async () => { 36 | const sfnClient = new SFNClient({}); 37 | 38 | const describeStepFunctionResult = await sfnClient.send( 39 | new DescribeStateMachineCommand({ 40 | stateMachineArn: process.env.BRANCHING_LOGIC_E2E_STEP_FUNCTION_ARN 41 | }) 42 | ); 43 | const stepFunctionDefinition = 44 | describeStepFunctionResult.definition as string; 45 | const stepFunctionRoleARN = describeStepFunctionResult.roleArn as string; 46 | 47 | const sfnLocalClient = new SFNClient({ 48 | endpoint: `http://localhost:${container?.getMappedPort(8083)}` 49 | }); 50 | 51 | const createLocalSFNResult = await sfnLocalClient.send( 52 | new CreateStateMachineCommand({ 53 | definition: stepFunctionDefinition, 54 | name: "BranchingLogic", 55 | roleArn: stepFunctionRoleARN 56 | }) 57 | ); 58 | 59 | const startLocalSFNExecutionResult = await sfnLocalClient.send( 60 | new StartExecutionCommand({ 61 | stateMachineArn: `${ 62 | createLocalSFNResult.stateMachineArn as string 63 | }#ErrorPath`, 64 | input: JSON.stringify({ 65 | firstName: "John", 66 | lastName: "Doe" 67 | }) 68 | }) 69 | ); 70 | 71 | await expect({ 72 | region: process.env.AWS_REGION, 73 | table: process.env.BRANCHING_LOGIC_E2E_DATA_TABLE_NAME 74 | }).toHaveItem( 75 | { 76 | PK: `USER#${startLocalSFNExecutionResult.executionArn}` 77 | }, 78 | { 79 | PK: `USER#${startLocalSFNExecutionResult.executionArn}`, 80 | firstName: "John", 81 | lastName: "Doe", 82 | backgroundCheck: "ERROR" 83 | } 84 | ); 85 | }, 50_000); 86 | -------------------------------------------------------------------------------- /lib/branching-logic-e2e.ts: -------------------------------------------------------------------------------- 1 | import { 2 | aws_dynamodb, 3 | aws_lambda_nodejs, 4 | aws_stepfunctions, 5 | aws_stepfunctions_tasks 6 | } from "aws-cdk-lib"; 7 | import { Construct } from "constructs"; 8 | import { join } from "path"; 9 | import { CfnOutput } from "./cfn-output"; 10 | 11 | export class BranchingLogicE2E extends Construct { 12 | constructor(scope: Construct, id: string) { 13 | super(scope, id); 14 | 15 | const dataTable = new aws_dynamodb.Table(this, "DataTable", { 16 | partitionKey: { 17 | name: "PK", 18 | type: aws_dynamodb.AttributeType.STRING 19 | }, 20 | billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST 21 | }); 22 | new CfnOutput(this, "BranchingLogicE2eDataTableName", { 23 | value: dataTable.tableName 24 | }); 25 | 26 | const transformDataStep = new aws_stepfunctions.Pass( 27 | this, 28 | "TransformData", 29 | { 30 | parameters: { 31 | firstName: aws_stepfunctions.JsonPath.stringAt("$.firstName"), 32 | lastName: aws_stepfunctions.JsonPath.stringAt("$.lastName"), 33 | PK: aws_stepfunctions.JsonPath.stringAt( 34 | "States.Format('USER#{}', $$.Execution.Id)" 35 | ) 36 | } 37 | } 38 | ); 39 | 40 | const backgroundCheckFunction = new aws_lambda_nodejs.NodejsFunction( 41 | this, 42 | "BackgroundCheckFunction", 43 | { 44 | entry: join(__dirname, "../functions/background-check.ts"), 45 | handler: "handler" 46 | } 47 | ); 48 | 49 | const invokeBackgroundCheckFunctionStep = 50 | new aws_stepfunctions_tasks.LambdaInvoke(this, "BackgroundCheckStep", { 51 | lambdaFunction: backgroundCheckFunction, 52 | payloadResponseOnly: true, 53 | resultPath: "$.backgroundCheck" 54 | }); 55 | 56 | const appendBackgroundCheckErrorStep = new aws_stepfunctions.Pass( 57 | this, 58 | "AppendBackgroundCheckError", 59 | { 60 | parameters: { 61 | status: "ERROR" 62 | }, 63 | resultPath: "$.backgroundCheck" 64 | } 65 | ); 66 | 67 | const saveUserStep = new aws_stepfunctions_tasks.DynamoPutItem( 68 | this, 69 | "SaveTheUser", 70 | { 71 | item: { 72 | firstName: aws_stepfunctions_tasks.DynamoAttributeValue.fromString( 73 | aws_stepfunctions.JsonPath.stringAt("$.firstName") 74 | ), 75 | lastName: aws_stepfunctions_tasks.DynamoAttributeValue.fromString( 76 | aws_stepfunctions.JsonPath.stringAt("$.lastName") 77 | ), 78 | backgroundCheck: 79 | aws_stepfunctions_tasks.DynamoAttributeValue.fromString( 80 | aws_stepfunctions.JsonPath.stringAt("$.backgroundCheck.status") 81 | ), 82 | PK: aws_stepfunctions_tasks.DynamoAttributeValue.fromString( 83 | aws_stepfunctions.JsonPath.stringAt("$.PK") 84 | ) 85 | }, 86 | table: dataTable 87 | } 88 | ); 89 | 90 | const stepFunctionDefinition = transformDataStep 91 | .next( 92 | invokeBackgroundCheckFunctionStep.addCatch( 93 | appendBackgroundCheckErrorStep.next(saveUserStep), 94 | { 95 | resultPath: aws_stepfunctions.JsonPath.DISCARD 96 | } 97 | ) 98 | ) 99 | .next(saveUserStep); 100 | 101 | const stepFunction = new aws_stepfunctions.StateMachine( 102 | this, 103 | "StepFunction", 104 | { 105 | definition: stepFunctionDefinition 106 | } 107 | ); 108 | new CfnOutput(this, "BranchingLogicE2eStepFunctionArn", { 109 | value: stepFunction.stateMachineArn 110 | }); 111 | } 112 | } 113 | --------------------------------------------------------------------------------