Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor 14 #71

Merged
merged 7 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
build/
examples/
bin/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
build/
bin/
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
build/
bin/
33 changes: 26 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This package lets you run AWS Step Functions state machines completely locally,
## Table of contents

- [Features](#features)
- [Use cases](#use-cases)
- [Installation](#installation)
- [Importing](#importing)
- [Node.js](#nodejs)
Expand All @@ -50,6 +51,16 @@ This package lets you run AWS Step Functions state machines completely locally,

To see the list of features defined in the specification that have full support, partial support, or no support, refer to [this document](/docs/feature-support.md).

## Use cases

Why would you want to use this package? Below is a non-exhaustive list of use cases for `aws-local-stepfunctions`:

- Testing state machines changes locally before deploying them to AWS.
- Testing the integration between a state machine and the Lambda functions associated with it in `Task` states.
- Debugging the code of associated Lambda functions interactively using the [`Task` state resource override feature](/docs/feature-support.md#task-state-resource-override).
- Debugging a state machine by using the [event logs feature](/docs/feature-support.md#execution-event-logs), to better understand the transitions between states and how data flows between them.
- Running state machines in the browser (not possible with [AWS Step Functions Local](https://docs.aws.amazon.com/step-functions/latest/dg/sfn-local.html)).

## Installation

```sh
Expand Down Expand Up @@ -104,7 +115,7 @@ The constructor takes the following parameters:
- `checkPaths`: If set to `false`, won't validate JSONPaths.
- `checkArn`: If set to `false`, won't validate ARN syntax in `Task` states.
- `noValidate`: If set to `true`, will skip validation of the definition entirely.
> NOTE: Use this option at your own risk, there are no guarantees when passing an invalid/non-standard definition to the state machine. Running it might result in undefined behavior.
> NOTE: Use this option at your own risk, there are no guarantees when passing an invalid or non-standard definition to the state machine. Running it might result in undefined/unsupported behavior.
- `awsConfig?`: An object that specifies the [AWS region and credentials](/docs/feature-support.md#providing-aws-credentials-and-region-to-execute-lambda-functions-specified-in-task-states) to use when invoking a Lambda function in a `Task` state. If not set, the AWS config will be resolved based on the [credentials provider chain](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html) of the AWS SDK for JavaScript V3. You don't need to use this option if you have a [shared config/credentials file](https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html) (for example, if you have the [AWS CLI](https://aws.amazon.com/cli/) installed) or if you use a local override for all of your `Task` states.
- `region`: The AWS region where the Lambda functions are created.
- `credentials`: An object that specifies which type of credentials to use.
Expand Down Expand Up @@ -138,11 +149,7 @@ const stateMachine = new StateMachine(machineDefinition, {

### `StateMachine.run(input[, options])`

Runs the state machine with the given `input` parameter and returns an object with the following properties:

- `result`: A `Promise` that resolves with the result of the execution once it terminates.
- `abort`: A function that takes no parameters and doesn't return any value. If called, [aborts the execution](/docs/feature-support.md#abort-a-running-execution) and throws an `ExecutionAbortedError`, unless the `noThrowOnAbort` option is set.
- `eventLogs`: An `AsyncGenerator` that [produces a log of events](/docs/feature-support.md#execution-event-logs) as the execution runs. To learn more about the events, their type, and their format, see the [following document](/docs/execution-event-logs.md).
Runs the state machine with the given `input`.

Each execution is independent of all others, meaning that you can concurrently call this method as many times as needed, without worrying about race conditions.

Expand All @@ -156,6 +163,14 @@ Each execution is independent of all others, meaning that you can concurrently c
- `noThrowOnAbort?`: If this option is set to `true`, aborting the execution will simply return `null` as result instead of throwing.
- `context?`: An object that will be used as the [Context Object](https://docs.aws.amazon.com/step-functions/latest/dg/input-output-contextobject.html) for the execution. If not passed, the Context Object will default to an empty object. This option is useful to mock the Context Object in case your definition references it in a JSONPath.

#### Return value

Returns an object that has the following properties:

- `result`: A `Promise` that resolves with the result of the execution, if it ends successfully.
- `abort`: A function that takes no parameters and doesn't return any value. If called, [aborts the execution](/docs/feature-support.md#abort-a-running-execution) and throws an `ExecutionAbortedError`, unless the `noThrowOnAbort` option is set.
- `eventLogs`: An `AsyncGenerator` that [produces a log of events](/docs/feature-support.md#execution-event-logs) as the execution runs. To learn more about the events, their type, and their format, see the [following document](/docs/execution-event-logs.md).

#### Basic example:

```js
Expand Down Expand Up @@ -383,8 +398,12 @@ Before attempting to run the state machine with the given inputs, the state mach

- JSONPath strings are valid.
- ARNs in the `Resource` field of `Task` states are valid.
- There are no invalid fields.
- All states in the definition can be reached.

If any of these checks fail, `local-sfn` will print the validation error and exit. To partially suppress this behavior, you can pass the `--no-jsonpath-validation` option, to suppress JSONPath validation; and the `--no-arn-validation` option, to suppress ARN validation.

If any of these two checks fail, `local-sfn` will print the validation error and exit. To suppress this behavior, you can pass the `--no-jsonpath-validation` option, to suppress JSONPath validation; and the `--no-arn-validation` option, to suppress ARN validation.
Alternatively, if you want to completely disable all validations, you can pass the `--no-validation` option. Be aware that passing this option implies no guarantees if the provided definition is invalid or contains non-standard fields: running it might result in undefined/unsupported behavior, so use at your own risk.

### Exit codes

Expand Down
29 changes: 28 additions & 1 deletion __tests__/cli/CLI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('CLI', () => {

await expect(program.parseAsync([], { from: 'user' })).rejects.toThrow();
expect(helpStr).toBe(
'Usage: local-sfn [options] [inputs...]\n\nExecute an Amazon States Language state machine with the given inputs.\nThe result of each execution will be output in a new line and in the same order\nas its corresponding input.\n\nArguments:\n inputs Input data for the state machine, can be any\n valid JSON value. Each input represents a\n state machine execution.\n \n When reading from the standard input, if the\n first line can be parsed as a single JSON\n value, then each line will be considered as an\n input. Otherwise, the entire standard input\n will be considered as a single JSON input.\n\nOptions:\n -V, --version Print the version number and exit.\n -d, --definition <definition> A JSON definition of a state machine.\n -f, --definition-file <path> Path to a file containing a JSON state machine\n definition.\n -t, --override-task <mapping> Override a Task state to run an executable\n file or script, instead of calling the service\n specified in the \'Resource\' field of the state\n definition. The mapping value has to be\n provided in the format\n [TaskStateToOverride]:[path/to/override/script].\n The override script will be passed the input\n of the Task state as first argument, which can\n then be used to compute the task result. The\n script must print the task result as a JSON\n value to the standard output.\n -w, --override-wait <mapping> Override a Wait state to pause for the\n specified amount of milliseconds, instead of\n pausing for the duration specified in the\n state definition. The mapping value has to be\n provided in the format\n [WaitStateToOverride]:[number].\n --context <json> A JSON object that will be passed to each\n execution as the context object.\n --context-file <path> Path to a file containing a JSON object that\n will be passed to each execution as the\n context object.\n --no-jsonpath-validation Disable validation of JSONPath strings in the\n state machine definition.\n --no-arn-validation Disable validation of ARNs in the state\n machine definition.\n -h, --help Print help for command and exit.\n\nExit codes:\n 0 All executions ran successfully.\n 1 An error occurred before the state machine could be executed.\n 2 At least one execution had an error.\n\nExample calls:\n $ local-sfn -f state-machine.json \'{ "num1": 2, "num2": 2 }\'\n $ local-sfn -f state-machine.json -t SendRequest:./override.sh -w WaitResponse:2000 \'{ "num1": 2, "num2": 2 }\'\n $ cat inputs.txt | local-sfn -f state-machine.json\n'
'Usage: local-sfn [options] [inputs...]\n\nExecute an Amazon States Language state machine with the given inputs.\nThe result of each execution will be output in a new line and in the same order\nas its corresponding input.\n\nArguments:\n inputs Input data for the state machine, can be any\n valid JSON value. Each input represents a\n state machine execution.\n \n When reading from the standard input, if the\n first line can be parsed as a single JSON\n value, then each line will be considered as an\n input. Otherwise, the entire standard input\n will be considered as a single JSON input.\n\nOptions:\n -V, --version Print the version number and exit.\n -d, --definition <definition> A JSON definition of a state machine.\n -f, --definition-file <path> Path to a file containing a JSON state machine\n definition.\n -t, --override-task <mapping> Override a Task state to run an executable\n file or script, instead of calling the service\n specified in the \'Resource\' field of the state\n definition. The mapping value has to be\n provided in the format\n [TaskStateToOverride]:[path/to/override/script].\n The override script will be passed the input\n of the Task state as first argument, which can\n then be used to compute the task result. The\n script must print the task result as a JSON\n value to the standard output.\n -w, --override-wait <mapping> Override a Wait state to pause for the\n specified amount of milliseconds, instead of\n pausing for the duration specified in the\n state definition. The mapping value has to be\n provided in the format\n [WaitStateToOverride]:[number].\n --context <json> A JSON object that will be passed to each\n execution as the context object.\n --context-file <path> Path to a file containing a JSON object that\n will be passed to each execution as the\n context object.\n --no-jsonpath-validation Disable validation of JSONPath strings in the\n state machine definition.\n --no-arn-validation Disable validation of ARNs in the state\n machine definition.\n --no-validation Disable validation of the state machine\n definition entirely. Use this option at your\n own risk, there are no guarantees when passing\n an invalid or non-standard definition to the\n state machine. Running it might result in\n undefined/unsupported behavior.\n -h, --help Print help for command and exit.\n\nExit codes:\n 0 All executions ran successfully.\n 1 An error occurred before the state machine could be executed.\n 2 At least one execution had an error.\n\nExample calls:\n $ local-sfn -f state-machine.json \'{ "num1": 2, "num2": 2 }\'\n $ local-sfn -f state-machine.json -t SendRequest:./override.sh -w WaitResponse:2000 \'{ "num1": 2, "num2": 2 }\'\n $ cat inputs.txt | local-sfn -f state-machine.json\n'
);
});
});
Expand Down Expand Up @@ -275,6 +275,33 @@ describe('CLI', () => {

expect(consoleLogMock).toHaveBeenCalled();
});

test('should NOT print error when passing a definition that does not conform to the spec and --no-validation option is passed', async () => {
const consoleLogMock = jest.fn();
jest.spyOn(LambdaClient.prototype, 'invokeFunction').mockImplementation(jest.fn());
jest.spyOn(console, 'log').mockImplementation(consoleLogMock);

const definition = `{
"StartAt": "AddNumbers",
"InvalidTopLevelField": false,
"States": {
"AddNumbers": {
"Type": "Task",
"Resource": "invalid-arn",
"End": true
},
"UnreachableState": {
"Type": "CustomType"
}
}
}`;

const program = makeProgram();

await program.parseAsync(['-d', definition, '--no-validation', '{}'], { from: 'user' });

expect(consoleLogMock).toHaveBeenCalled();
});
});

describe('State machine execution', () => {
Expand Down
19 changes: 6 additions & 13 deletions __tests__/stateActions/WaitStateAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,13 @@ describe('Wait State', () => {
const abortSignal = new AbortController().signal;
const options = {
abortSignal,
rootAbortSignal: undefined,
waitTimeOverrideOption: undefined,
};

const waitStateAction = new WaitStateAction(definition, stateName);
const { stateResult } = await waitStateAction.execute(input, context, options);

expect(mockSleepFunction).toHaveBeenCalledWith(10000, abortSignal, undefined);
expect(mockSleepFunction).toHaveBeenCalledWith(10000, abortSignal);
expect(stateResult).toEqual({ prop1: 'test', prop2: 12345 });
});

Expand All @@ -50,14 +49,13 @@ describe('Wait State', () => {
const abortSignal = new AbortController().signal;
const options = {
abortSignal,
rootAbortSignal: undefined,
waitTimeOverrideOption: undefined,
};

const waitStateAction = new WaitStateAction(definition, stateName);
const { stateResult } = await waitStateAction.execute(input, context, options);

expect(mockSleepFunction).toHaveBeenCalledWith(20700000, abortSignal, undefined);
expect(mockSleepFunction).toHaveBeenCalledWith(20700000, abortSignal);
expect(stateResult).toEqual({ prop1: 'test', prop2: 12345 });
});

Expand All @@ -73,14 +71,13 @@ describe('Wait State', () => {
const abortSignal = new AbortController().signal;
const options = {
abortSignal,
rootAbortSignal: undefined,
waitTimeOverrideOption: undefined,
};

const waitStateAction = new WaitStateAction(definition, stateName);
const { stateResult } = await waitStateAction.execute(input, context, options);

expect(mockSleepFunction).toHaveBeenCalledWith(10000, abortSignal, undefined);
expect(mockSleepFunction).toHaveBeenCalledWith(10000, abortSignal);
expect(stateResult).toEqual({ waitFor: 10 });
});

Expand All @@ -96,14 +93,13 @@ describe('Wait State', () => {
const abortSignal = new AbortController().signal;
const options = {
abortSignal,
rootAbortSignal: undefined,
waitTimeOverrideOption: undefined,
};

const waitStateAction = new WaitStateAction(definition, stateName);
const { stateResult } = await waitStateAction.execute(input, context, options);

expect(mockSleepFunction).toHaveBeenCalledWith(20700000, abortSignal, undefined);
expect(mockSleepFunction).toHaveBeenCalledWith(20700000, abortSignal);
expect(stateResult).toEqual({ waitUntil: '2022-12-05T05:45:00Z' });
});

Expand All @@ -117,14 +113,13 @@ describe('Wait State', () => {
const input = { waitUntil: '2022-12-05T05:45:00Z' };
const context = {};
const abortSignal = new AbortController().signal;
const rootAbortSignal = new AbortController().signal;
const options = { waitTimeOverrideOption: 1500, abortSignal, rootAbortSignal };
const options = { waitTimeOverrideOption: 1500, abortSignal };

const waitStateAction = new WaitStateAction(definition, stateName);
const { stateResult } = await waitStateAction.execute(input, context, options);

expect(mockSleepFunction).toHaveBeenCalledTimes(1);
expect(mockSleepFunction).toHaveBeenCalledWith(1500, abortSignal, rootAbortSignal);
expect(mockSleepFunction).toHaveBeenCalledWith(1500, abortSignal);
expect(stateResult).toEqual({ waitUntil: '2022-12-05T05:45:00Z' });
});

Expand All @@ -140,7 +135,6 @@ describe('Wait State', () => {
const abortSignal = new AbortController().signal;
const options = {
abortSignal,
rootAbortSignal: undefined,
waitTimeOverrideOption: undefined,
};

Expand All @@ -163,7 +157,6 @@ describe('Wait State', () => {
const abortSignal = new AbortController().signal;
const options = {
abortSignal,
rootAbortSignal: undefined,
waitTimeOverrideOption: undefined,
};

Expand Down
10 changes: 0 additions & 10 deletions __tests__/util/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,6 @@ describe('Utils', () => {

await expect(sleepPromise).resolves.not.toThrow();
});

test('should resolve promise when sleep is aborted using `rootAbortController` argument', async () => {
const abortController = new AbortController();
const rootAbortController = new AbortController();

const sleepPromise = sleep(1000, abortController.signal, rootAbortController.signal);
rootAbortController.abort();

await expect(sleepPromise).resolves.not.toThrow();
});
});

describe('isRFC3339Timestamp', () => {
Expand Down
2 changes: 2 additions & 0 deletions docs/execution-event-logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
- [`StateCaught` event](#statecaught-event)
- [Helper data types](#helper-data-types)
- [`StateData`](#statedata)
- [`RetryData`](#retrydata)
- [`CatchData`](#catchdata)

## Events

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
},
"license": "MIT",
"files": [
"build"
"build",
"bin"
],
"type": "module",
"types": "build/main.d.ts",
Expand All @@ -40,7 +41,7 @@
"node": ">=16.0.0"
},
"bin": {
"local-sfn": "./build/CLI.cjs"
"local-sfn": "./bin/CLI.cjs"
},
"scripts": {
"test": "jest",
Expand Down
Loading