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

Release 1.1.0 #72

Merged
merged 63 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
321f603
Fix typos
nibble-4bits Sep 4, 2023
6eec2a4
Pass context object to JsonPath evaluation in Choice rules
nibble-4bits Sep 10, 2023
e1f354e
Merge pull request #64 from nibble-4bits/fix/pass-context-object-choi…
nibble-4bits Sep 10, 2023
24620a2
Polyfill `cause` property of `Error` object
nibble-4bits Sep 10, 2023
75edd9a
Throw `LambdaInvocationError` if Lambda invocation fails
nibble-4bits Sep 10, 2023
895eeff
Create util variable to know if code is running in browser environment
nibble-4bits Sep 10, 2023
2d5713e
Throw error if running in browser and an AWS config was not passed
nibble-4bits Sep 10, 2023
f5f79a7
Merge pull request #65 from nibble-4bits/feature/lambda-error-cause
nibble-4bits Sep 11, 2023
d5d4cd3
Add `types` property to `exports` field
nibble-4bits Sep 4, 2023
ccea8f3
Allow passing non-async functions to task state overrides
nibble-4bits Sep 4, 2023
f0d4ad8
Export types from API methods parameters
nibble-4bits Sep 4, 2023
4f72190
Add `engines` field to lockfile
nibble-4bits Sep 6, 2023
2b07bb9
Upgrade dependencies to latest version
nibble-4bits Sep 6, 2023
007d675
Remove optional and `undefined` typings
nibble-4bits Sep 10, 2023
125b850
Remove leftover console logs
nibble-4bits Sep 10, 2023
dcce1dc
Merge pull request #66 from nibble-4bits/refactor-13
nibble-4bits Sep 11, 2023
549dfa5
Move `JsonPath.ts` file to new `jsonPath` directory
nibble-4bits Sep 14, 2023
d1295fe
Remove optionality from `context` parameter
nibble-4bits Sep 14, 2023
81a878b
Create util function to verify if a date complies with RFC3339 grammar
nibble-4bits Sep 14, 2023
0efdfa9
Create abstract JSONPath constraint class to verify if a JSONPath ful…
nibble-4bits Sep 15, 2023
344a0e6
Create constraint to verify if a JSONPath result is an array
nibble-4bits Sep 15, 2023
f477934
Create constraint to verify if a JSONPath result is a boolean
nibble-4bits Sep 15, 2023
d27ed3b
Create constraint to verify if a JSONPath points to a defined value
nibble-4bits Sep 15, 2023
b8e97a1
Create constraint to verify if a JSONPath result is an integer
nibble-4bits Sep 15, 2023
09dfc86
Create constraint to verify if a JSONPath result is a number
nibble-4bits Sep 15, 2023
df56140
Create constraint to verify if a JSONPath result is an RFC3339 timestamp
nibble-4bits Sep 15, 2023
493d34d
Create constraint to verify if a JSONPath result is a string
nibble-4bits Sep 15, 2023
275d350
Validate constraints when evaluating JSONPath
nibble-4bits Sep 15, 2023
ede9594
Add constraints to JSONPath queries in `Wait` state
nibble-4bits Sep 15, 2023
c21c4ed
Add constraints to JSONPath query in `Map` state
nibble-4bits Sep 15, 2023
20c5e41
Add constraints to JSONPath queries in `Choice` state
nibble-4bits Sep 15, 2023
3225043
Create util function to quote a JSON value
nibble-4bits Sep 15, 2023
15286a3
Quote expected value in JSONPath constraints error messages
nibble-4bits Sep 15, 2023
55ad82f
Refer to paths as `Paths` instead of `JSONPaths` in constraints error…
nibble-4bits Sep 15, 2023
d75c34e
Make `IsPresent` operator ignore the default defined value constraint
nibble-4bits Sep 15, 2023
598ed3e
Update test assertion to account for array constraint of `ItemsPath` …
nibble-4bits Sep 15, 2023
7387ebf
Rename function to `stringifyJSONValue`
nibble-4bits Sep 16, 2023
dc64d94
Add test for util function `isRFC3339Date`
nibble-4bits Sep 16, 2023
4ed3146
Rename function to `isRFC3339Timestamp`
nibble-4bits Sep 16, 2023
6a4148d
Add tests for JSONPath constraints and invalid evaluation
nibble-4bits Sep 16, 2023
8686fcd
Merge pull request #68 from nibble-4bits/fix/jsonpath-invalid-evaluation
nibble-4bits Sep 16, 2023
77e4b26
Create new types for `StateFailed`, `StateRetried`, and `StateCaught`…
nibble-4bits Sep 16, 2023
ecce533
Create helper functions to dispatch new state events
nibble-4bits Sep 16, 2023
2906a4b
Dispatch `StateFailed`, `StateRetried`, and `StateCaught` events duri…
nibble-4bits Sep 16, 2023
03864cd
Add documentation for new `StateFailed`, `StateRetried`, and `StateCa…
nibble-4bits Sep 16, 2023
32a55d5
Update tests to validate new `StateFailed`, `StateRetried`, and `Stat…
nibble-4bits Sep 16, 2023
d2d3dd9
Merge pull request #69 from nibble-4bits/feature/state-failed-retried…
nibble-4bits Sep 16, 2023
9836945
Expose `noValidate` option publicly
nibble-4bits Sep 16, 2023
91ec097
Document `noValidate` option for state machine constructor
nibble-4bits Sep 16, 2023
b4b6d52
Add example for `noValidate` option
nibble-4bits Sep 16, 2023
71e712f
Add test to validate constructor doesn't throw if `noValidate` option…
nibble-4bits Sep 16, 2023
316859d
Merge pull request #70 from nibble-4bits/feature/no-validate-option
nibble-4bits Sep 16, 2023
31d8bf5
Output CLI script to `bin` directory
nibble-4bits Sep 15, 2023
40574ff
Abort child signal in `run` method to avoid passing it to `Wait` stat…
nibble-4bits Sep 16, 2023
f7fe166
Add `RetryData` and `CatchData` to table of contents
nibble-4bits Sep 16, 2023
6212e13
Add CLI option to disable validation of state machine definition enti…
nibble-4bits Sep 17, 2023
1bafee1
Add JSDoc comments to constructor and run options
nibble-4bits Sep 17, 2023
d5d965f
Move explanation of `StateMachine.run` return value to its own header…
nibble-4bits Sep 18, 2023
05ba0ac
Add list of use cases
nibble-4bits Sep 18, 2023
55e809f
Merge pull request #71 from nibble-4bits/refactor-14
nibble-4bits Sep 18, 2023
197b956
Replace usage of incorrect word
nibble-4bits Sep 18, 2023
2201832
Update value of `bin` field in lockfile
nibble-4bits Sep 19, 2023
998905b
Bump package version to v1.1.0
nibble-4bits Sep 19, 2023
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: 27 additions & 6 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 @@ -103,6 +114,8 @@ The constructor takes the following parameters:
- `validationOptions?`: An object that specifies how the definition should be validated.
- `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 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 @@ -136,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 @@ -154,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 @@ -381,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
78 changes: 58 additions & 20 deletions __tests__/EventLogger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import type {
ExecutionStartedEvent,
ExecutionSucceededEvent,
ExecutionTerminatedEvent,
StateEvent,
StateEnteredEvent,
StateExitedEvent,
} from '../src/typings/EventLogs';
import { StatesRuntimeError } from '../src/error/predefined/StatesRuntimeError';
import { EventLogger } from '../src/stateMachine/EventLogger';
import './_customMatchers';

Expand All @@ -27,34 +29,70 @@ describe('Event Logger', () => {

eventLogger.dispatchExecutionStartedEvent(50);
eventLogger.dispatchStateEnteredEvent('SomeState', 'Choice', { a: 1, b: 'string', c: true, d: [1, 2, 3] });
eventLogger.dispatchStateFailedEvent(
'A state name',
'Parallel',
12345,
new StatesRuntimeError('An error happened during runtime')
);
eventLogger.dispatchStateRetriedEvent('A state name', 'Parallel', 12345, { ErrorEquals: ['States.ALL'] }, 2);
eventLogger.dispatchStateCaughtEvent('A state name', 'Parallel', 12345, {
ErrorEquals: ['States.ALL'],
Next: 'CatchState',
});
eventLogger.dispatchStateExitedEvent('AnotherState', 'Task', ['a', 'b', 1, null], 123.456);
eventLogger.dispatchExecutionSucceededEvent('result');

const firstEvent = await generator.next();
const secondEvent = await generator.next();
const thirdEvent = await generator.next();
const fourthEvent = await generator.next();
const endEvent = await generator.next();

expect(firstEvent.value).toEqual({ type: 'ExecutionStarted', timestamp: 1670198400000, input: 50 });
expect(secondEvent.value).toEqual({
const event1 = await generator.next();
const event2 = await generator.next();
const event3 = await generator.next();
const event4 = await generator.next();
const event5 = await generator.next();
const event6 = await generator.next();
const event7 = await generator.next();
const event8 = await generator.next();

expect(event1.value).toEqual({ type: 'ExecutionStarted', timestamp: 1670198400000, input: 50 });
expect(event2.value).toEqual({
type: 'StateEntered',
timestamp: 1670198400000,
state: { name: 'SomeState', type: 'Choice', input: { a: 1, b: 'string', c: true, d: [1, 2, 3] } },
});
expect(thirdEvent.value).toEqual({
expect(event3.value).toEqual({
type: 'StateFailed',
timestamp: 1670198400000,
state: { name: 'A state name', type: 'Parallel', input: 12345 },
Error: 'States.Runtime',
Cause: 'An error happened during runtime',
});
expect(event4.value).toEqual({
type: 'StateRetried',
timestamp: 1670198400000,
state: { name: 'A state name', type: 'Parallel', input: 12345 },
retry: { retrier: { ErrorEquals: ['States.ALL'] }, attempt: 2 },
});
expect(event5.value).toEqual({
type: 'StateCaught',
timestamp: 1670198400000,
state: { name: 'A state name', type: 'Parallel', input: 12345 },
catch: { catcher: { ErrorEquals: ['States.ALL'], Next: 'CatchState' } },
});
expect(event6.value).toEqual({
type: 'StateExited',
timestamp: 1670198400000,
state: { name: 'AnotherState', type: 'Task', input: ['a', 'b', 1, null], output: 123.456 },
});
expect(fourthEvent.value).toEqual({ type: 'ExecutionSucceeded', timestamp: 1670198400000, output: 'result' });
expect(endEvent.value).toBeUndefined();

expect(firstEvent.done).toBe(false);
expect(secondEvent.done).toBe(false);
expect(thirdEvent.done).toBe(false);
expect(fourthEvent.done).toBe(false);
expect(endEvent.done).toBe(true);
expect(event7.value).toEqual({ type: 'ExecutionSucceeded', timestamp: 1670198400000, output: 'result' });
expect(event8.value).toBeUndefined();

expect(event1.done).toBe(false);
expect(event2.done).toBe(false);
expect(event3.done).toBe(false);
expect(event4.done).toBe(false);
expect(event5.done).toBe(false);
expect(event6.done).toBe(false);
expect(event7.done).toBe(false);
expect(event8.done).toBe(true);
});
});

Expand Down Expand Up @@ -126,7 +164,7 @@ describe('Event Logger', () => {
test('should forward `StateEntered` event and add index', async () => {
const eventLogger = new EventLogger();
const generator = eventLogger.getEvents();
const event: StateEvent = {
const event: StateEnteredEvent = {
type: 'StateEntered',
timestamp: Date.now(),
state: { name: 'SomeEvent', type: 'Succeed', input: {} },
Expand All @@ -148,7 +186,7 @@ describe('Event Logger', () => {
test('should forward `StateExited` event and add index', async () => {
const eventLogger = new EventLogger();
const generator = eventLogger.getEvents();
const event: StateEvent = {
const event: StateExitedEvent = {
type: 'StateExited',
timestamp: Date.now(),
state: { name: 'SomeEvent', type: 'Succeed', input: {}, output: {} },
Expand Down
42 changes: 28 additions & 14 deletions __tests__/InputOutputProcessing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ describe('Input processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processInputPath(undefined, input);
const result = processInputPath(undefined, input, context);

expect(result).toEqual(input);
});
Expand All @@ -54,8 +55,9 @@ describe('Input processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processInputPath('$.movies', input);
const result = processInputPath('$.movies', input, context);

expect(result).toEqual([
{
Expand Down Expand Up @@ -89,8 +91,9 @@ describe('Input processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processInputPath(null, input);
const result = processInputPath(null, input, context);

expect(result).toEqual({});
});
Expand All @@ -108,8 +111,9 @@ describe('Input processing', () => {
},
};
const input = {};
const context = {};

const result = processPayloadTemplate(parameters, input);
const result = processPayloadTemplate(parameters, input, context);

expect(result).toEqual({
field1: 50,
Expand Down Expand Up @@ -149,8 +153,9 @@ describe('Input processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processPayloadTemplate(parameters, input);
const result = processPayloadTemplate(parameters, input, context);

expect(result).toEqual({
field1: 50,
Expand Down Expand Up @@ -192,8 +197,9 @@ describe('Input processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processPayloadTemplate(parameters, input);
const result = processPayloadTemplate(parameters, input, context);

expect(result).toEqual({
field1: 50,
Expand Down Expand Up @@ -247,8 +253,9 @@ describe('Input processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processPayloadTemplate(parameters, input);
const result = processPayloadTemplate(parameters, input, context);

expect(result).toEqual({
field1: 50,
Expand Down Expand Up @@ -279,8 +286,9 @@ describe('Output processing', () => {
},
};
const input = {};
const context = {};

const result = processPayloadTemplate(resultSelector, input);
const result = processPayloadTemplate(resultSelector, input, context);

expect(result).toEqual({
field1: 50,
Expand Down Expand Up @@ -320,8 +328,9 @@ describe('Output processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processPayloadTemplate(resultSelector, input);
const result = processPayloadTemplate(resultSelector, input, context);

expect(result).toEqual({
field1: 50,
Expand Down Expand Up @@ -363,8 +372,9 @@ describe('Output processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processPayloadTemplate(resultSelector, input);
const result = processPayloadTemplate(resultSelector, input, context);

expect(result).toEqual({
field1: 50,
Expand Down Expand Up @@ -418,8 +428,9 @@ describe('Output processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processPayloadTemplate(resultSelector, input);
const result = processPayloadTemplate(resultSelector, input, context);

expect(result).toEqual({
field1: 50,
Expand Down Expand Up @@ -621,8 +632,9 @@ describe('Output processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processOutputPath(undefined, input);
const result = processOutputPath(undefined, input, context);

expect(result).toEqual(input);
});
Expand All @@ -645,8 +657,9 @@ describe('Output processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processOutputPath('$.movies', input);
const result = processOutputPath('$.movies', input, context);

expect(result).toEqual([
{
Expand Down Expand Up @@ -680,8 +693,9 @@ describe('Output processing', () => {
lastUpdated: '2020-05-27T08:00:00Z',
},
};
const context = {};

const result = processOutputPath(null, input);
const result = processOutputPath(null, input, context);

expect(result).toEqual({});
});
Expand Down
Loading