aws-local-stepfunctions 0.6.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -4
- package/build/CLI.cjs +279 -0
- package/build/main.browser.esm.js +349 -98
- package/build/main.d.ts +97 -21
- package/build/main.node.cjs +349 -98
- package/build/main.node.esm.js +349 -98
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -2,7 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
A TypeScript implementation of the [Amazon States Language specification](https://states-language.net/spec.html).
|
|
4
4
|
|
|
5
|
-
This package lets you run AWS Step Functions completely locally, both in Node.js and in the browser!
|
|
5
|
+
This package lets you run AWS Step Functions state machines completely locally, both in Node.js and in the browser!
|
|
6
|
+
|
|
7
|
+
<p align="left">
|
|
8
|
+
<a href="/LICENSE">
|
|
9
|
+
<img alt="Project license (MIT)" src="https://img.shields.io/github/license/nibble-4bits/aws-local-stepfunctions">
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://github.com/nibble-4bits/aws-local-stepfunctions/actions/workflows/pr-run-tests.yml">
|
|
12
|
+
<img alt="GitHub Workflow Status (with event)" src="https://img.shields.io/github/actions/workflow/status/nibble-4bits/aws-local-stepfunctions/pr-run-tests.yml">
|
|
13
|
+
</a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/aws-local-stepfunctions">
|
|
15
|
+
<img alt="Latest npm version" src="https://img.shields.io/npm/v/aws-local-stepfunctions">
|
|
16
|
+
</a>
|
|
17
|
+
<a href="https://www.npmjs.com/package/aws-local-stepfunctions">
|
|
18
|
+
<img alt="node-lts" src="https://img.shields.io/node/v-lts/aws-local-stepfunctions">
|
|
19
|
+
</a>
|
|
20
|
+
<a href="https://www.npmjs.com/package/aws-local-stepfunctions">
|
|
21
|
+
<img alt="npm type definitions" src="https://img.shields.io/npm/types/aws-local-stepfunctions">
|
|
22
|
+
</a>
|
|
23
|
+
</p>
|
|
6
24
|
|
|
7
25
|
## Table of contents
|
|
8
26
|
|
|
@@ -16,6 +34,15 @@ This package lets you run AWS Step Functions completely locally, both in Node.js
|
|
|
16
34
|
- [API](#api)
|
|
17
35
|
- [Constructor](#constructor-new-statemachinedefinition-statemachineoptions)
|
|
18
36
|
- [StateMachine.run](#statemachineruninput-options)
|
|
37
|
+
- [CLI](#cli)
|
|
38
|
+
- [Basic usage](#basic-usage)
|
|
39
|
+
- [Passing input from stdin](#passing-input-from-stdin)
|
|
40
|
+
- [Overriding Task and Wait states](#overriding-task-and-wait-states)
|
|
41
|
+
- [Task state override](#task-state-override)
|
|
42
|
+
- [Wait state override](#wait-state-override)
|
|
43
|
+
- [Passing a custom Context Object](#passing-a-custom-context-object)
|
|
44
|
+
- [Disabling ASL validations](#disabling-asl-validations)
|
|
45
|
+
- [Exit codes](#exit-codes)
|
|
19
46
|
- [Examples](#examples)
|
|
20
47
|
- [License](#license)
|
|
21
48
|
|
|
@@ -49,7 +76,7 @@ import { StateMachine } from 'aws-local-stepfunctions';
|
|
|
49
76
|
|
|
50
77
|
You can import the bundled package directly into a browser script as an ES module, from one of the following CDNs:
|
|
51
78
|
|
|
52
|
-
> NOTE: The following examples will import the latest package version. Refer to the CDNs websites to know about other ways in which you can specify the package URL (for example, to import a specific version).
|
|
79
|
+
> NOTE: The following examples will import the latest package version. Refer to the CDNs websites to know about other ways in which you can specify the package URL (for example, to import a specific version or a minified version).
|
|
53
80
|
|
|
54
81
|
#### [unpkg](https://unpkg.com/)
|
|
55
82
|
|
|
@@ -111,8 +138,9 @@ const stateMachine = new StateMachine(machineDefinition, {
|
|
|
111
138
|
|
|
112
139
|
Runs the state machine with the given `input` parameter and returns an object with the following properties:
|
|
113
140
|
|
|
141
|
+
- `result`: A `Promise` that resolves with the result of the execution once it terminates.
|
|
114
142
|
- `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.
|
|
115
|
-
- `
|
|
143
|
+
- `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).
|
|
116
144
|
|
|
117
145
|
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.
|
|
118
146
|
|
|
@@ -122,8 +150,9 @@ Each execution is independent of all others, meaning that you can concurrently c
|
|
|
122
150
|
- `options?`:
|
|
123
151
|
- `overrides?`: An object to override the behavior of certain states:
|
|
124
152
|
- `taskResourceLocalHandlers?`: An [object that overrides](/docs/feature-support.md#task-state-resource-override) the resource of the specified `Task` states to run a local function.
|
|
125
|
-
- `waitTimeOverrides?`: An [object that overrides](/docs/feature-support.md#wait-state-duration-override) the wait duration of the specified `Wait` states. The
|
|
153
|
+
- `waitTimeOverrides?`: An [object that overrides](/docs/feature-support.md#wait-state-duration-override) the wait duration of the specified `Wait` states. The specified override duration should be in milliseconds.
|
|
126
154
|
- `noThrowOnAbort?`: If this option is set to `true`, aborting the execution will simply return `null` as result instead of throwing.
|
|
155
|
+
- `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.
|
|
127
156
|
|
|
128
157
|
#### Basic example:
|
|
129
158
|
|
|
@@ -149,6 +178,222 @@ const result = await execution.result; // wait until the execution finishes to g
|
|
|
149
178
|
console.log(result); // log the result of the execution
|
|
150
179
|
```
|
|
151
180
|
|
|
181
|
+
## CLI
|
|
182
|
+
|
|
183
|
+
In addition to the JavaScript API, `aws-local-stepfunctions` also provides a command-line interface. The CLI allows you to run one or several executions without having to create a Node.js script.
|
|
184
|
+
|
|
185
|
+
To use the CLI as a global shell command, you need to install the package globally:
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
npm install -g aws-local-stepfunctions
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
After installing the package, the command `local-sfn` will be available in your shell.
|
|
192
|
+
|
|
193
|
+
### Basic usage
|
|
194
|
+
|
|
195
|
+
The simplest way to use the CLI is by passing either the `-d, --definition` or the `-f, --definition-file` option, along with the input(s) for the state machine. For example:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
local-sfn \
|
|
199
|
+
-f state-machine.json \
|
|
200
|
+
'{ "num1": 1, "num2": 2 }' \
|
|
201
|
+
'{ "num1": 3, "num2": 4 }' \
|
|
202
|
+
'{ "num1": 5, "num2": 6 }'
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This command would execute the state machine defined in file `state-machine.json` with `'{ "num1": 1, "num2": 2 }'`, `'{ "num1": 3, "num2": 4 }'`, and `'{ "num1": 5, "num2": 6 }'` as inputs. Each input corresponds to a state machine execution, and each execution is run independently, so the failure of one execution doesn't mean the failure of all of the other executions.
|
|
206
|
+
|
|
207
|
+
Now, suppose the state machine in file `state-machine.json` is defined as a single `Task` state that calls a Lambda function that adds `num1` and `num2`:
|
|
208
|
+
|
|
209
|
+
<a id="cli-state-machine"></a>
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"StartAt": "AddNumbers",
|
|
214
|
+
"States": {
|
|
215
|
+
"AddNumbers": {
|
|
216
|
+
"Type": "Task",
|
|
217
|
+
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:AddNumbers",
|
|
218
|
+
"End": true
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Then, the output of the `local-sfn` command above may look something like this:
|
|
225
|
+
|
|
226
|
+
```sh
|
|
227
|
+
3
|
|
228
|
+
7
|
|
229
|
+
11
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Note that each line of the output corresponds to the result of each input, in the same order that the inputs were given to the command.
|
|
233
|
+
|
|
234
|
+
### Passing input from stdin
|
|
235
|
+
|
|
236
|
+
`local-sfn` can also read the execution input from the standard input.
|
|
237
|
+
|
|
238
|
+
If the first line of stdin can be parsed as a single JSON value, then `local-sfn` will consider each line as an input. Otherwise, the entire stdin will be considered as a single JSON input.
|
|
239
|
+
|
|
240
|
+
For example, assume you have the following text file, called `inputs.txt`, and you want to pass the contents of the file as inputs to `local-sfn`:
|
|
241
|
+
|
|
242
|
+
```txt
|
|
243
|
+
{ "num1": 1, "num2": 2 }
|
|
244
|
+
{ "num1": 3, "num2": 4 }
|
|
245
|
+
{ "num1": 5, "num2": 6 }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Because the first line is parsable as JSON, `local-sfn` will process each line as a single input.
|
|
249
|
+
|
|
250
|
+
You can then run the following command to pass the inputs of the text file to `local-sfn`:
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
cat inputs.txt | local-sfn -f state-machine.json
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Alternatively, using input redirection:
|
|
257
|
+
|
|
258
|
+
```sh
|
|
259
|
+
local-sfn -f state-machine.json < inputs.txt
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
On the other hand, assume you have this additional file, called `input.json`:
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{
|
|
266
|
+
"num1": 5,
|
|
267
|
+
"num2": 3
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
If you pass this file as input, `local-sfn` will automatically detect that it is a single, multiline JSON value and process it as a single value.
|
|
272
|
+
|
|
273
|
+
### Overriding Task and Wait states
|
|
274
|
+
|
|
275
|
+
As explained in the Feature support document, it's possible to override the default actions of [`Task` states](/docs/feature-support.md#task-state-resource-override) and [`Wait` states](/docs/feature-support.md#wait-state-duration-override).
|
|
276
|
+
|
|
277
|
+
#### Task state override
|
|
278
|
+
|
|
279
|
+
To override a `Task` state, pass the `-t, --override-task` option. This option takes as value the name of the `Task` state you want to override, and a path to a script or program that will be executed instead of the resource specified in the state definition. The state name and the path must be separated by a colon `:`.
|
|
280
|
+
|
|
281
|
+
Using the same [state machine definition](#cli-state-machine) as before, if you wanted to override the `AddNumbers` state to run a custom script, you can do it like this:
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
local-sfn -f state-machine.json -t AddNumbers:./override.sh '{ "num1": 1, "num2": 2 }'
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
This command would run the state machine, but instead of invoking the Lambda function specified in the `Resource` field of the `AddNumbers` state, the `override.sh` script would be executed.
|
|
288
|
+
|
|
289
|
+
Now, suppose the `override.sh` script is defined like this:
|
|
290
|
+
|
|
291
|
+
```sh
|
|
292
|
+
#!/bin/sh
|
|
293
|
+
|
|
294
|
+
TASK_INPUT=$1 # First argument is the input to the overridden Task state
|
|
295
|
+
echo "$TASK_INPUT" | jq '.num1 + .num2' # Use jq to add "num1" and "num2", and print result to stdout
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
When overriding a `Task` state, the overriding executable will be passed the input to the `Task` state as first argument, which can then be used to compute the task result. Similarly, the executable must print the task result as a JSON value to the standard output, so `local-sfn` can then read stdout and use the value as the result of the `Task` state. If the executable terminates with an exit code different from `0`, its standard error will be printed and the execution will be marked as a failure.
|
|
299
|
+
|
|
300
|
+
Additionally, you can pass the `-t, --override-task` option multiple times, to override more than one `Task` state. For example:
|
|
301
|
+
|
|
302
|
+
```sh
|
|
303
|
+
local-sfn
|
|
304
|
+
-f state-machine.json \
|
|
305
|
+
-t AddNumbers:./override.sh \
|
|
306
|
+
-t SendRequest:./request.py \
|
|
307
|
+
-t ProcessImage:./proc_image \
|
|
308
|
+
'{ "num1": 1, "num2": 2 }'
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
This command would execute the state machine, and override `Task` states `AddNumbers`, `SendRequest`, and `ProcessImage` to run the `override.sh` shell script, the `request.py` Python script, and the `proc_image` program, respectively.
|
|
312
|
+
|
|
313
|
+
#### Wait state override
|
|
314
|
+
|
|
315
|
+
To override the duration of a `Wait` state, pass the `-w, --override-wait` option. This option takes as value the name of the `Wait` state you want to override, and a number that represents the amount in milliseconds that you want to pause the execution for. The state name and the milliseconds amount must be separated by a colon `:`.
|
|
316
|
+
|
|
317
|
+
For example:
|
|
318
|
+
|
|
319
|
+
```sh
|
|
320
|
+
local-sfn -f state-machine.json -w WaitResponse:1500 '{ "num1": 1, "num2": 2 }'
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
This command would execute the state machine, and when entering the `WaitResponse` `Wait` state, the execution would be paused for 1500 milliseconds (1.5 seconds), disregarding the `Seconds`, `Timestamp`, `SecondsPath`, or `TimestampPath` fields that could've been specified in the definition of `WaitResponse`.
|
|
324
|
+
|
|
325
|
+
In the same way as the `-t, --override-task` option, you can pass the `-w, --override-wait` option multiple times, to override more than one `Wait` state. For example:
|
|
326
|
+
|
|
327
|
+
```sh
|
|
328
|
+
local-sfn \
|
|
329
|
+
-f state-machine.json \
|
|
330
|
+
-w WaitResponse:1500 \
|
|
331
|
+
-w PauseUntilSignal:250 \
|
|
332
|
+
-w Delay:0 \
|
|
333
|
+
'{ "num1": 1, "num2": 2 }'
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
This command would execute the state machine, and override `Wait` states `WaitResponse` and `PauseUntilSignal` to pause the execution for 1500 and 250 milliseconds, respectively. The `Delay` state wouldn't be paused at all, since the override value is set to 0.
|
|
337
|
+
|
|
338
|
+
### Passing a custom Context Object
|
|
339
|
+
|
|
340
|
+
If the JSONPaths in your definition reference the Context Object, you can provide a mock Context Object by passing either the `--context` or the `--context-file` option. For example, given the following definition:
|
|
341
|
+
|
|
342
|
+
```json
|
|
343
|
+
{
|
|
344
|
+
"StartAt": "Get execution context data",
|
|
345
|
+
"States": {
|
|
346
|
+
"Get execution context data": {
|
|
347
|
+
"Type": "Pass",
|
|
348
|
+
"Parameters": {
|
|
349
|
+
"execId.$": "$$.Execution.Id",
|
|
350
|
+
"execName.$": "$$.Execution.Name"
|
|
351
|
+
},
|
|
352
|
+
"End": true
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
And given the following `context.json` file:
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
{
|
|
362
|
+
"Execution": {
|
|
363
|
+
"Id": "arn:aws:states:us-east-1:123456789012:execution:stateMachineName:executionName",
|
|
364
|
+
"Name": "executionName"
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
You could provide the Context Object to the execution in the following manner:
|
|
370
|
+
|
|
371
|
+
```sh
|
|
372
|
+
local-sfn \
|
|
373
|
+
-f state-machine.json \
|
|
374
|
+
--context-file context.json \
|
|
375
|
+
'{}'
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Disabling ASL validations
|
|
379
|
+
|
|
380
|
+
Before attempting to run the state machine with the given inputs, the state machine definition itself is validated to check that:
|
|
381
|
+
|
|
382
|
+
- JSONPath strings are valid.
|
|
383
|
+
- ARNs in the `Resource` field of `Task` states are valid.
|
|
384
|
+
|
|
385
|
+
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.
|
|
386
|
+
|
|
387
|
+
### Exit codes
|
|
388
|
+
|
|
389
|
+
`local-sfn` can terminate with the following exit codes:
|
|
390
|
+
|
|
391
|
+
| Exit code | Explanation |
|
|
392
|
+
| :-------: | ------------------------------------------------------------------------------------ |
|
|
393
|
+
| 0 | The state machine was executed, and all executions ran successfully. |
|
|
394
|
+
| 1 | An error occurred before the state machine could be executed (e.g. a parsing error). |
|
|
395
|
+
| 2 | The state machine was executed, but at least one execution had an error. |
|
|
396
|
+
|
|
152
397
|
## Examples
|
|
153
398
|
|
|
154
399
|
You can check more examples and options usage in the [examples](/examples) directory.
|
package/build/CLI.cjs
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/cli/CLI.ts
|
|
32
|
+
var CLI_exports = {};
|
|
33
|
+
__export(CLI_exports, {
|
|
34
|
+
makeProgram: () => makeProgram
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(CLI_exports);
|
|
37
|
+
var import_readline = __toESM(require("readline"), 1);
|
|
38
|
+
var import_commander = require("commander");
|
|
39
|
+
|
|
40
|
+
// package.json
|
|
41
|
+
var version = "1.0.0";
|
|
42
|
+
|
|
43
|
+
// src/cli/ArgumentParsers.ts
|
|
44
|
+
var import_fs = require("fs");
|
|
45
|
+
var import_child_process = require("child_process");
|
|
46
|
+
|
|
47
|
+
// src/util/index.ts
|
|
48
|
+
function tryJSONParse(jsonStr) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(jsonStr);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/cli/ArgumentParsers.ts
|
|
57
|
+
function parseDefinitionOption(command, definition) {
|
|
58
|
+
const jsonOrError = tryJSONParse(definition);
|
|
59
|
+
if (jsonOrError instanceof Error) {
|
|
60
|
+
command.error(
|
|
61
|
+
`error: parsing of state machine definition passed in option '-d, --definition <definition>' failed: ${jsonOrError.message}`,
|
|
62
|
+
{
|
|
63
|
+
exitCode: 1 /* PreExecutionFailure */
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return jsonOrError;
|
|
68
|
+
}
|
|
69
|
+
function parseDefinitionFileOption(command, definitionFile) {
|
|
70
|
+
let file;
|
|
71
|
+
try {
|
|
72
|
+
file = (0, import_fs.readFileSync)(definitionFile).toString();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
command.error(`error: ${error.message}`, { exitCode: 1 /* PreExecutionFailure */ });
|
|
75
|
+
}
|
|
76
|
+
const jsonOrError = tryJSONParse(file);
|
|
77
|
+
if (jsonOrError instanceof Error) {
|
|
78
|
+
command.error(
|
|
79
|
+
`error: parsing of state machine definition in file '${definitionFile}' failed: ${jsonOrError.message}`,
|
|
80
|
+
{ exitCode: 1 /* PreExecutionFailure */ }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return jsonOrError;
|
|
84
|
+
}
|
|
85
|
+
function parseOverrideTaskOption(value, previous = {}) {
|
|
86
|
+
const [taskStateName, scriptPath] = value.split(":");
|
|
87
|
+
previous[taskStateName] = (input) => {
|
|
88
|
+
const spawnResult = (0, import_child_process.spawnSync)(scriptPath, [JSON.stringify(input)]);
|
|
89
|
+
if (spawnResult.error) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Attempt to run task override '${scriptPath}' for state '${taskStateName}' failed: ${spawnResult.error.message}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (spawnResult.status !== 0) {
|
|
95
|
+
throw new Error(`${scriptPath} ('${taskStateName}'): ${spawnResult.stderr.toString()}`);
|
|
96
|
+
}
|
|
97
|
+
const overrideResult = spawnResult.stdout.toString();
|
|
98
|
+
const jsonOrError = tryJSONParse(overrideResult);
|
|
99
|
+
if (jsonOrError instanceof Error) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Parsing of output '${overrideResult}' from task override '${scriptPath}' for state '${taskStateName}' failed: ${jsonOrError.message}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return Promise.resolve(jsonOrError);
|
|
105
|
+
};
|
|
106
|
+
return previous;
|
|
107
|
+
}
|
|
108
|
+
function parseOverrideWaitOption(value, previous = {}) {
|
|
109
|
+
const [waitStateName, duration] = value.split(":");
|
|
110
|
+
previous[waitStateName] = Number(duration);
|
|
111
|
+
return previous;
|
|
112
|
+
}
|
|
113
|
+
function parseContextOption(command, context) {
|
|
114
|
+
const jsonOrError = tryJSONParse(context);
|
|
115
|
+
if (jsonOrError instanceof Error) {
|
|
116
|
+
command.error(
|
|
117
|
+
`error: parsing of context object passed in option '--context <json>' failed: ${jsonOrError.message}`,
|
|
118
|
+
{
|
|
119
|
+
exitCode: 1 /* PreExecutionFailure */
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return jsonOrError;
|
|
124
|
+
}
|
|
125
|
+
function parseContextFileOption(command, contextFile) {
|
|
126
|
+
let file;
|
|
127
|
+
try {
|
|
128
|
+
file = (0, import_fs.readFileSync)(contextFile).toString();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
command.error(`error: ${error.message}`, { exitCode: 1 /* PreExecutionFailure */ });
|
|
131
|
+
}
|
|
132
|
+
const jsonOrError = tryJSONParse(file);
|
|
133
|
+
if (jsonOrError instanceof Error) {
|
|
134
|
+
command.error(`error: parsing of context object in file '${contextFile}' failed: ${jsonOrError.message}`, {
|
|
135
|
+
exitCode: 1 /* PreExecutionFailure */
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return jsonOrError;
|
|
139
|
+
}
|
|
140
|
+
function parseInputArguments(command, value, previous = []) {
|
|
141
|
+
const jsonOrError = tryJSONParse(value);
|
|
142
|
+
if (jsonOrError instanceof Error) {
|
|
143
|
+
command.error(`error: parsing of input value '${value}' failed: ${jsonOrError.message}`, {
|
|
144
|
+
exitCode: 1 /* PreExecutionFailure */
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return previous.concat(jsonOrError);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/cli/CommandHandler.ts
|
|
151
|
+
var import_main = require("./main.node.cjs");
|
|
152
|
+
async function commandAction(inputs, options, command) {
|
|
153
|
+
let stateMachine;
|
|
154
|
+
try {
|
|
155
|
+
stateMachine = new import_main.StateMachine(options.definition ?? options.definitionFile, {
|
|
156
|
+
validationOptions: {
|
|
157
|
+
checkPaths: options.jsonpathValidation,
|
|
158
|
+
checkArn: options.arnValidation
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
command.error(`error: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
const resultsPromises = inputs.map((input) => {
|
|
165
|
+
const { result } = stateMachine.run(input, {
|
|
166
|
+
overrides: {
|
|
167
|
+
taskResourceLocalHandlers: options.overrideTask,
|
|
168
|
+
waitTimeOverrides: options.overrideWait
|
|
169
|
+
},
|
|
170
|
+
context: options.context ?? options.contextFile
|
|
171
|
+
});
|
|
172
|
+
return result;
|
|
173
|
+
});
|
|
174
|
+
const results = await Promise.allSettled(resultsPromises);
|
|
175
|
+
let exitCode = 0 /* Success */;
|
|
176
|
+
for (const result of results) {
|
|
177
|
+
if (result.status === "fulfilled") {
|
|
178
|
+
console.log(result.value);
|
|
179
|
+
} else {
|
|
180
|
+
exitCode = 2 /* StateMachineExecutionFailure */;
|
|
181
|
+
let msg = result.reason.message;
|
|
182
|
+
if (result.reason instanceof import_main.ExecutionTimeoutError) {
|
|
183
|
+
msg = "Execution timed out";
|
|
184
|
+
}
|
|
185
|
+
console.log(msg.trim());
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
process.exitCode = exitCode;
|
|
189
|
+
}
|
|
190
|
+
function preActionHook(thisCommand) {
|
|
191
|
+
const options = thisCommand.opts();
|
|
192
|
+
if (thisCommand.args.length === 0) {
|
|
193
|
+
thisCommand.help();
|
|
194
|
+
}
|
|
195
|
+
if (!options["definition"] && !options["definitionFile"]) {
|
|
196
|
+
thisCommand.error(
|
|
197
|
+
"error: missing either option '-d, --definition <definition>' or option '-f, --definition-file <path>'",
|
|
198
|
+
{ exitCode: 1 /* PreExecutionFailure */ }
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/cli/CLI.ts
|
|
204
|
+
function makeProgram() {
|
|
205
|
+
const command = new import_commander.Command();
|
|
206
|
+
command.name("local-sfn").description(
|
|
207
|
+
`Execute an Amazon States Language state machine with the given inputs.
|
|
208
|
+
The result of each execution will be output in a new line and in the same order as its corresponding input.`
|
|
209
|
+
).helpOption("-h, --help", "Print help for command and exit.").configureHelp({ helpWidth: 80 }).addHelpText(
|
|
210
|
+
"after",
|
|
211
|
+
`
|
|
212
|
+
Exit codes:
|
|
213
|
+
0 All executions ran successfully.
|
|
214
|
+
1 An error occurred before the state machine could be executed.
|
|
215
|
+
2 At least one execution had an error.
|
|
216
|
+
|
|
217
|
+
Example calls:
|
|
218
|
+
$ local-sfn -f state-machine.json '{ "num1": 2, "num2": 2 }'
|
|
219
|
+
$ local-sfn -f state-machine.json -t SendRequest:./override.sh -w WaitResponse:2000 '{ "num1": 2, "num2": 2 }'
|
|
220
|
+
$ cat inputs.txt | local-sfn -f state-machine.json`
|
|
221
|
+
).version(version, "-V, --version", "Print the version number and exit.").addOption(
|
|
222
|
+
new import_commander.Option("-d, --definition <definition>", "A JSON definition of a state machine.").conflicts("definition-file").argParser((value) => parseDefinitionOption(command, value))
|
|
223
|
+
).addOption(
|
|
224
|
+
new import_commander.Option("-f, --definition-file <path>", "Path to a file containing a JSON state machine definition.").conflicts("definition").argParser((value) => parseDefinitionFileOption(command, value))
|
|
225
|
+
).addOption(
|
|
226
|
+
new import_commander.Option(
|
|
227
|
+
"-t, --override-task <mapping>",
|
|
228
|
+
"Override a Task state to run an executable file or script, instead of calling the service specified in the 'Resource' field of the state definition. The mapping value has to be provided in the format [TaskStateToOverride]:[path/to/override/script]. The override script will be passed the input of the Task state as first argument, which can then be used to compute the task result. The script must print the task result as a JSON value to the standard output."
|
|
229
|
+
).argParser(parseOverrideTaskOption)
|
|
230
|
+
).addOption(
|
|
231
|
+
new import_commander.Option(
|
|
232
|
+
"-w, --override-wait <mapping>",
|
|
233
|
+
"Override a Wait state to pause for the specified amount of milliseconds, instead of pausing for the duration specified in the state definition. The mapping value has to be provided in the format [WaitStateToOverride]:[number]."
|
|
234
|
+
).argParser(parseOverrideWaitOption)
|
|
235
|
+
).addOption(
|
|
236
|
+
new import_commander.Option(
|
|
237
|
+
"--context <json>",
|
|
238
|
+
"A JSON object that will be passed to each execution as the context object."
|
|
239
|
+
).argParser((value) => parseContextOption(command, value))
|
|
240
|
+
).addOption(
|
|
241
|
+
new import_commander.Option(
|
|
242
|
+
"--context-file <path>",
|
|
243
|
+
"Path to a file containing a JSON object that will be passed to each execution as the context object."
|
|
244
|
+
).argParser((value) => parseContextFileOption(command, value))
|
|
245
|
+
).addOption(
|
|
246
|
+
new import_commander.Option("--no-jsonpath-validation", "Disable validation of JSONPath strings in the state machine definition.")
|
|
247
|
+
).addOption(new import_commander.Option("--no-arn-validation", "Disable validation of ARNs in the state machine definition.")).argument(
|
|
248
|
+
"[inputs...]",
|
|
249
|
+
"Input data for the state machine, can be any valid JSON value. Each input represents a state machine execution.\n\nWhen reading from the standard input, if the first line can be parsed as a single JSON value, then each line will be considered as an input. Otherwise, the entire standard input will be considered as a single JSON input.",
|
|
250
|
+
(value, previous) => parseInputArguments(command, value, previous)
|
|
251
|
+
).hook("preAction", preActionHook).action(commandAction);
|
|
252
|
+
return command;
|
|
253
|
+
}
|
|
254
|
+
if (require.main === module) {
|
|
255
|
+
(async function() {
|
|
256
|
+
const program = makeProgram();
|
|
257
|
+
if (process.stdin.isTTY) {
|
|
258
|
+
await program.parseAsync();
|
|
259
|
+
} else {
|
|
260
|
+
const rl = import_readline.default.createInterface({
|
|
261
|
+
input: process.stdin
|
|
262
|
+
});
|
|
263
|
+
const stdin = [];
|
|
264
|
+
for await (const line of rl) {
|
|
265
|
+
stdin.push(line);
|
|
266
|
+
}
|
|
267
|
+
const firstLineJSON = tryJSONParse(stdin[0]);
|
|
268
|
+
if (firstLineJSON instanceof Error) {
|
|
269
|
+
await program.parseAsync([...process.argv, stdin.join("").trim()]);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
await program.parseAsync([...process.argv, ...stdin.filter((line) => line)]);
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
}
|
|
276
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
277
|
+
0 && (module.exports = {
|
|
278
|
+
makeProgram
|
|
279
|
+
});
|