Skip to content
Open
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
50 changes: 34 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,31 @@ await sm.startExecution('world');

Must be called before `startExecution`, binds to `Tasks` and replaces their handler to the provided `Callback` parameter.

The first argument may be the **state name** or the task's **Resource ARN**. The second may be a function, or a Serverless-style handler reference (`'path/to/file.exportedFn'` or `{ handler: 'path/to/file.exportedFn' }`) which is loaded and invoked — pass `handlerBasePath` to the constructor to control where those paths resolve from:

```js
const sm = new Sfn({ StateMachine, handlerBasePath: __dirname });
sm.bindTaskResource('HelloWorld', 'handlers/hello.handler'); // runs the real handler
```

### trace(Input, Options)

Run an execution and walk it generator-style, yielding each transition. The iterator's return value is the final output.

```js
for await (const step of sm.trace('world')) {
console.log(step.state, step.step); // e.g. 'TaskStateEntered' 'HelloWorld'
}
```

### CLI

```
stepfunctions run definition.json --input '{"hello":"world"}' [--report]
```

Runs a state machine definition file and prints the result as JSON. Tasks without a bound/real handler pass their input through. Use `--base-path` to resolve Serverless-style handler references.

### getExecutionResult()

Must be called after `startExecution`. This function returns the absolute result from the statemachine if it has finished. It is idempotent — repeated calls return the same value. (`startExecution` also resolves to this value, so you often don't need it.)
Expand Down Expand Up @@ -154,12 +179,7 @@ Set `QueryLanguage: "JSONata"` on the state machine or an individual state. `{%

`Assign` is supported in both JSONPath and JSONata modes. Assigned variables are referenced as `$name` in later states and follow AWS scoping: a `Map`/`Parallel` child can read outer variables but its own assignments do not leak back to the parent scope.

**Experimental**

- Retry
- Catch

The above features are labeled experimental because it cannot be fully spec compliant(yet) due to AWS specific cases.
`Retry` and `Catch` are supported on `Task`, `Map` and `Parallel` (error matching, `States.ALL`, `IntervalSeconds`/`BackoffRate`/`MaxAttempts`, and `ResultPath` for the caught error). They remain best-effort for some AWS-specific error names.

**Not yet supported**

Expand All @@ -172,18 +192,16 @@ The above features are labeled experimental because it cannot be fully spec comp
will be used within a testing context. You can override this behaviour by adding the `respectTime` option to true in the `startExecution` method.
2. No support for Handling `States.Permissions` as the library will not have context on AWS related permissions.

## Future

PR's are welcome to help finish the ones below :)
## Done

- [ ] Change arn in bindTaskResource instead of the State name
- [ ] Run `sls invoke local` instead of binding resolvers
- [x] Bind in `bindTaskResource` by the Resource ARN as well as the state name
- [x] Run real handler modules (Serverless-style `file.export` refs) instead of binding resolvers
- [x] Typescript typings
- [ ] Run via CLI
- [ ] Remove the "experimental" label on retry and catch
- [ ] More accurate timing mechanism
- [ ] use `jest.fakeTimers()` in the test
- [ ] Walk through states ala "generator" style. e.g, `yield sm.next()`
- [x] Run via CLI
- [x] Remove the "experimental" label on retry and catch
- [x] More accurate timing mechanism
- [x] use `jest.fakeTimers()` in the test
- [x] Walk through states ala "generator" style (`for await (const step of sm.trace(input))`)
- [x] Support the JSONata query language and Variables (`Assign`)

## License
Expand Down
69 changes: 69 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const path = require('path');
const Sfn = require('../index.js');

const USAGE = `Usage: stepfunctions run <definition.json> [options]

Options:
--input <json> JSON input passed to the first state
--input-file <path> Read the JSON input from a file
--base-path <dir> Base dir for Serverless-style handler refs (default: cwd)
--report Print the transition report (console.table)
-h, --help Show this help
`;

async function main(argv) {
const args = argv.slice(2);
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
process.stdout.write(USAGE);
return undefined;
}
if (args[0] !== 'run' || !args[1]) {
process.stderr.write(USAGE);
process.exitCode = 1;
return undefined;
}

const file = args[1];
let inputJson;
let report = false;
let basePath = process.cwd();
for (let i = 2; i < args.length; i++) {
const flag = args[i];
if (flag === '--input') {
inputJson = args[++i];
} else if (flag === '--input-file') {
inputJson = fs.readFileSync(args[++i], 'utf8');
} else if (flag === '--base-path') {
basePath = args[++i];
} else if (flag === '--report') {
report = true;
} else {
process.stderr.write(`Unknown option: ${flag}\n`);
process.exitCode = 1;
return undefined;
}
}

const definition = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8'));
const input = inputJson === undefined ? undefined : JSON.parse(inputJson);
const sm = new Sfn({ StateMachine: definition, handlerBasePath: basePath });
const result = await sm.startExecution(input);
if (report) {
sm.getReport();
}
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return result;
}

if (require.main === module) {
main(process.argv).catch((err) => {
process.stderr.write(`${err && err.message ? err.message : err}\n`);
process.exitCode = 1;
});
}

module.exports = { main };
35 changes: 31 additions & 4 deletions index.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,33 @@ export interface ExecutionOptions {
/** A bound Task handler, e.g. a Serverless/Lambda handler. */
export type TaskResource = (input: any) => any;

/**
* A bound resource: a handler function or a Serverless-style reference
* (`'path/to/file.exportedFn'` or `{ handler: 'path/to/file.exportedFn' }`).
*/
export type ResourceReference = TaskResource | string | { handler: string };

/** A single recorded transition yielded by `trace()`. */
export interface Transition {
state: string;
step: any;
input: any;
output: any;
elapsed: number;
[key: string]: any;
}

/** An Amazon States Language state machine definition. */
export type StateMachine = Record<string, any>;

export interface Options {
/** Friendly name for the execution. Defaults to the StateMachine `StartAt`. */
Name?: string;
StateMachine: StateMachine;
/** Map of Task state name to its handler. */
Resources?: Record<string, TaskResource>;
/** Map of Task state name (or Resource ARN) to its handler. */
Resources?: Record<string, ResourceReference>;
/** Base dir for resolving Serverless-style handler refs. Defaults to cwd. */
handlerBasePath?: string;
}

/**
Expand All @@ -37,14 +55,23 @@ export declare class StepFunction extends EventEmitter {
*/
startExecution(input?: any, opts?: ExecutionOptions): Promise<any>;

/**
* Run an execution and walk it generator-style, yielding each transition.
* The iterator's return value is the final output.
*/
trace(
input?: any,
opts?: ExecutionOptions,
): AsyncGenerator<Transition, any, void>;

/** Return the final output of the most recent execution. */
getExecutionResult(): any;

/** Pretty-print the recorded transitions with `console.table`. */
getReport(): void;

/** Replace a Task's resource with the provided handler. */
bindTaskResource(task: string, fn: TaskResource): void;
/** Bind a Task's resource (by state name or Resource ARN) to a handler. */
bindTaskResource(task: string, fn: ResourceReference): void;
}

export default StepFunction;
35 changes: 31 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,33 @@ declare namespace StepFunction {
/** A bound Task handler, e.g. a Serverless/Lambda handler. */
type TaskResource = (input: any) => any;

/**
* A bound resource: a handler function or a Serverless-style reference
* (`'path/to/file.exportedFn'` or `{ handler: 'path/to/file.exportedFn' }`).
*/
type ResourceReference = TaskResource | string | { handler: string };

/** A single recorded transition yielded by `trace()`. */
interface Transition {
state: string;
step: any;
input: any;
output: any;
elapsed: number;
[key: string]: any;
}

/** An Amazon States Language state machine definition. */
type StateMachine = Record<string, any>;

interface Options {
/** Friendly name for the execution. Defaults to the StateMachine `StartAt`. */
Name?: string;
StateMachine: StateMachine;
/** Map of Task state name to its handler. */
Resources?: Record<string, TaskResource>;
/** Map of Task state name (or Resource ARN) to its handler. */
Resources?: Record<string, ResourceReference>;
/** Base dir for resolving Serverless-style handler refs. Defaults to cwd. */
handlerBasePath?: string;
}
}

Expand All @@ -42,14 +60,23 @@ declare class StepFunction extends EventEmitter {
opts?: StepFunction.ExecutionOptions,
): Promise<any>;

/**
* Run an execution and walk it generator-style, yielding each transition.
* The iterator's return value is the final output.
*/
trace(
input?: any,
opts?: StepFunction.ExecutionOptions,
): AsyncGenerator<StepFunction.Transition, any, void>;

/** Return the final output of the most recent execution. */
getExecutionResult(): any;

/** Pretty-print the recorded transitions with `console.table`. */
getReport(): void;

/** Replace a Task's resource with the provided handler. */
bindTaskResource(task: string, fn: StepFunction.TaskResource): void;
/** Bind a Task's resource (by state name or Resource ARN) to a handler. */
bindTaskResource(task: string, fn: StepFunction.ResourceReference): void;
}

export = StepFunction;
Loading
Loading