From 31d8bf558e797c503128a45353bbd86a423d8404 Mon Sep 17 00:00:00 2001 From: Luis De Anda Date: Thu, 14 Sep 2023 21:40:43 -0600 Subject: [PATCH 1/7] Output CLI script to `bin` directory --- .eslintignore | 1 + .gitignore | 1 + .prettierignore | 1 + package.json | 5 +++-- tsup.config.ts | 5 +++-- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.eslintignore b/.eslintignore index e536614..5e3e920 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ build/ examples/ +bin/ diff --git a/.gitignore b/.gitignore index b38db2f..8dac27d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ build/ +bin/ diff --git a/.prettierignore b/.prettierignore index 567609b..d5697ec 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ build/ +bin/ diff --git a/package.json b/package.json index 31f5c7a..64d0f7b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ }, "license": "MIT", "files": [ - "build" + "build", + "bin" ], "type": "module", "types": "build/main.d.ts", @@ -40,7 +41,7 @@ "node": ">=16.0.0" }, "bin": { - "local-sfn": "./build/CLI.cjs" + "local-sfn": "./bin/CLI.cjs" }, "scripts": { "test": "jest", diff --git a/tsup.config.ts b/tsup.config.ts index d1b82c5..468bd9a 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -2,7 +2,6 @@ import { defineConfig, Options } from 'tsup'; function getCommonConfig(): Options { return { - outDir: 'build', splitting: false, }; } @@ -10,6 +9,7 @@ function getCommonConfig(): Options { function getPackageConfig(platform: 'node' | 'browser'): Options { return { ...getCommonConfig(), + outDir: 'build', name: platform, platform, entry: ['src/main.ts'], @@ -23,6 +23,7 @@ function getPackageConfig(platform: 'node' | 'browser'): Options { function getCLIConfig(): Options { return { ...getCommonConfig(), + outDir: 'bin', name: 'cli', entry: ['src/cli/CLI.ts'], format: ['cjs'], @@ -31,7 +32,7 @@ function getCLIConfig(): Options { name: 'rewrite-main-import', setup(build) { build.onResolve({ filter: /^\.\.\/main$/ }, () => { - return { path: './main.node.cjs', external: true }; + return { path: '../build/main.node.cjs', external: true }; }); }, }, From 40574ff19adbf3f3ff5bd0325b273a1a648af3da Mon Sep 17 00:00:00 2001 From: Luis De Anda Date: Fri, 15 Sep 2023 23:18:11 -0600 Subject: [PATCH 2/7] Abort child signal in `run` method to avoid passing it to `Wait` state action and `sleep` --- .../stateActions/WaitStateAction.test.ts | 19 ++++++------------- __tests__/util/index.test.ts | 10 ---------- src/stateMachine/StateExecutor.ts | 3 +-- src/stateMachine/StateMachine.ts | 13 +++++++++++++ .../stateActions/WaitStateAction.ts | 10 +++++----- src/typings/StateActions.ts | 1 - src/util/index.ts | 15 ++++----------- 7 files changed, 29 insertions(+), 42 deletions(-) diff --git a/__tests__/stateActions/WaitStateAction.test.ts b/__tests__/stateActions/WaitStateAction.test.ts index 126fccb..f0a3b73 100644 --- a/__tests__/stateActions/WaitStateAction.test.ts +++ b/__tests__/stateActions/WaitStateAction.test.ts @@ -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 }); }); @@ -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 }); }); @@ -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 }); }); @@ -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' }); }); @@ -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' }); }); @@ -140,7 +135,6 @@ describe('Wait State', () => { const abortSignal = new AbortController().signal; const options = { abortSignal, - rootAbortSignal: undefined, waitTimeOverrideOption: undefined, }; @@ -163,7 +157,6 @@ describe('Wait State', () => { const abortSignal = new AbortController().signal; const options = { abortSignal, - rootAbortSignal: undefined, waitTimeOverrideOption: undefined, }; diff --git a/__tests__/util/index.test.ts b/__tests__/util/index.test.ts index d428c91..2a44bc3 100644 --- a/__tests__/util/index.test.ts +++ b/__tests__/util/index.test.ts @@ -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', () => { diff --git a/src/stateMachine/StateExecutor.ts b/src/stateMachine/StateExecutor.ts index 102a07a..b956f0b 100644 --- a/src/stateMachine/StateExecutor.ts +++ b/src/stateMachine/StateExecutor.ts @@ -141,7 +141,7 @@ export class StateExecutor { if (shouldRetry) { const stateDefinition = this.stateDefinition as TaskState | MapState | ParallelState; - await sleep(waitTimeBeforeRetry!, options.abortSignal, options.runOptions?._rootAbortSignal); + await sleep(waitTimeBeforeRetry!, options.abortSignal); options.eventLogger.dispatchStateRetriedEvent( this.stateName, @@ -404,7 +404,6 @@ export class StateExecutor { const executionResult = await waitStateAction.execute(input, context, { waitTimeOverrideOption, abortSignal: options.abortSignal, - rootAbortSignal: options.runOptions?._rootAbortSignal, }); return executionResult; diff --git a/src/stateMachine/StateMachine.ts b/src/stateMachine/StateMachine.ts index f55eef8..a234966 100644 --- a/src/stateMachine/StateMachine.ts +++ b/src/stateMachine/StateMachine.ts @@ -66,6 +66,18 @@ export class StateMachine { const abortController = new AbortController(); const eventLogger = new EventLogger(); + let rootSignalAbortHandler: () => void; + if (options?._rootAbortSignal) { + rootSignalAbortHandler = () => abortController.abort(); + if (options._rootAbortSignal.aborted) { + // If root abort signal is already aborted, abort the signal in the current context of execution. + rootSignalAbortHandler(); + } else { + // Else, set a listener that aborts the current controller. + options._rootAbortSignal.addEventListener('abort', rootSignalAbortHandler); + } + } + let onAbortHandler: () => void; const settleOnAbort = new Promise((resolve, reject) => { if (options?.noThrowOnAbort) { @@ -107,6 +119,7 @@ export class StateMachine { }, () => { abortController.signal.removeEventListener('abort', onAbortHandler); + options?._rootAbortSignal?.removeEventListener('abort', rootSignalAbortHandler); clearTimeout(timeoutId); } ); diff --git a/src/stateMachine/stateActions/WaitStateAction.ts b/src/stateMachine/stateActions/WaitStateAction.ts index dff00bb..54c8db5 100644 --- a/src/stateMachine/stateActions/WaitStateAction.ts +++ b/src/stateMachine/stateActions/WaitStateAction.ts @@ -18,23 +18,23 @@ class WaitStateAction extends BaseStateAction { if (options.waitTimeOverrideOption !== undefined) { // If the wait time override is set, sleep for the specified number of milliseconds - await sleep(options.waitTimeOverrideOption, options.abortSignal, options.rootAbortSignal); + await sleep(options.waitTimeOverrideOption, options.abortSignal); return this.buildExecutionResult(input); } if (state.Seconds) { - await sleep(state.Seconds * 1000, options.abortSignal, options.rootAbortSignal); + await sleep(state.Seconds * 1000, options.abortSignal); } else if (state.Timestamp) { const dateTimestamp = new Date(state.Timestamp); const currentTime = Date.now(); const timeDiff = dateTimestamp.getTime() - currentTime; - await sleep(timeDiff, options.abortSignal, options.rootAbortSignal); + await sleep(timeDiff, options.abortSignal); } else if (state.SecondsPath) { const seconds = jsonPathQuery(state.SecondsPath, input, context, { constraints: [IntegerConstraint.greaterThanOrEqual(0)], }); - await sleep(seconds * 1000, options.abortSignal, options.rootAbortSignal); + await sleep(seconds * 1000, options.abortSignal); } else if (state.TimestampPath) { const timestamp = jsonPathQuery(state.TimestampPath, input, context, { constraints: [RFC3339TimestampConstraint], @@ -43,7 +43,7 @@ class WaitStateAction extends BaseStateAction { const currentTime = Date.now(); const timeDiff = dateTimestamp.getTime() - currentTime; - await sleep(timeDiff, options.abortSignal, options.rootAbortSignal); + await sleep(timeDiff, options.abortSignal); } return this.buildExecutionResult(input); diff --git a/src/typings/StateActions.ts b/src/typings/StateActions.ts index dd00ed2..04ac44e 100644 --- a/src/typings/StateActions.ts +++ b/src/typings/StateActions.ts @@ -32,7 +32,6 @@ export type PassStateActionOptions = Record; export type WaitStateActionOptions = { waitTimeOverrideOption: number | undefined; abortSignal: AbortSignal; - rootAbortSignal: AbortSignal | undefined; }; export type ChoiceStateActionOptions = Record; diff --git a/src/util/index.ts b/src/util/index.ts index 68ad024..2657833 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -18,32 +18,25 @@ export function isPlainObj(value: unknown): value is JSONObject { * Optionally, the pause can be canceled at any moment if an abort signal is passed. * @param ms Number of milliseconds to sleep. * @param abortSignal An abort signal that can cancel the sleep if the signal is aborted. - * @param rootAbortSignal The top-level state machine abort signal, that can cancel the sleep if the signal is aborted. */ -export function sleep(ms: number, abortSignal?: AbortSignal, rootAbortSignal?: AbortSignal) { +export function sleep(ms: number, abortSignal?: AbortSignal) { return new Promise((resolve) => { - // Resolve early if any of the abort signals have been aborted - if (abortSignal?.aborted || rootAbortSignal?.aborted) { + // Resolve early if the abort signal has been aborted already + if (abortSignal?.aborted) { return resolve(); } const onAbort = () => { - abortSignal?.removeEventListener('abort', onAbort); - rootAbortSignal?.removeEventListener('abort', onAbort); - clearTimeout(timeout); resolve(); }; const timeout = setTimeout(() => { abortSignal?.removeEventListener('abort', onAbort); - rootAbortSignal?.removeEventListener('abort', onAbort); - resolve(); }, ms); - abortSignal?.addEventListener('abort', onAbort); - rootAbortSignal?.addEventListener('abort', onAbort); + abortSignal?.addEventListener('abort', onAbort, { once: true }); }); } From f7fe1668019b3544807f817d6472e665c6003d4f Mon Sep 17 00:00:00 2001 From: Luis De Anda Date: Sat, 16 Sep 2023 14:52:14 -0600 Subject: [PATCH 3/7] Add `RetryData` and `CatchData` to table of contents --- docs/execution-event-logs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/execution-event-logs.md b/docs/execution-event-logs.md index cf2215d..7fe0558 100644 --- a/docs/execution-event-logs.md +++ b/docs/execution-event-logs.md @@ -22,6 +22,8 @@ - [`StateCaught` event](#statecaught-event) - [Helper data types](#helper-data-types) - [`StateData`](#statedata) + - [`RetryData`](#retrydata) + - [`CatchData`](#catchdata) ## Events From 6212e138917f7b7c62b898825aac77b42e429638 Mon Sep 17 00:00:00 2001 From: Luis De Anda Date: Sun, 17 Sep 2023 13:30:30 -0600 Subject: [PATCH 4/7] Add CLI option to disable validation of state machine definition entirely --- README.md | 8 ++++++-- __tests__/cli/CLI.test.ts | 29 ++++++++++++++++++++++++++++- src/cli/CLI.ts | 6 ++++++ src/cli/CommandHandler.ts | 1 + src/typings/CLI.ts | 1 + 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f064b41..a1d7ec4 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,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. @@ -383,8 +383,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 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. +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. + +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 diff --git a/__tests__/cli/CLI.test.ts b/__tests__/cli/CLI.test.ts index 247e7d9..d986467 100644 --- a/__tests__/cli/CLI.test.ts +++ b/__tests__/cli/CLI.test.ts @@ -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 A JSON definition of a state machine.\n -f, --definition-file Path to a file containing a JSON state machine\n definition.\n -t, --override-task 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 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 A JSON object that will be passed to each\n execution as the context object.\n --context-file 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 A JSON definition of a state machine.\n -f, --definition-file Path to a file containing a JSON state machine\n definition.\n -t, --override-task 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 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 A JSON object that will be passed to each\n execution as the context object.\n --context-file 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' ); }); }); @@ -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', () => { diff --git a/src/cli/CLI.ts b/src/cli/CLI.ts index 0b39d0d..452d83c 100644 --- a/src/cli/CLI.ts +++ b/src/cli/CLI.ts @@ -79,6 +79,12 @@ Example calls: new Option('--no-jsonpath-validation', 'Disable validation of JSONPath strings in the state machine definition.') ) .addOption(new Option('--no-arn-validation', 'Disable validation of ARNs in the state machine definition.')) + .addOption( + new Option( + '--no-validation', + 'Disable validation of the state machine definition entirely. 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.' + ) + ) .argument( '[inputs...]', '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.', diff --git a/src/cli/CommandHandler.ts b/src/cli/CommandHandler.ts index d9df0f5..553231f 100644 --- a/src/cli/CommandHandler.ts +++ b/src/cli/CommandHandler.ts @@ -11,6 +11,7 @@ async function commandAction(inputs: JSONValue[], options: ParsedCommandOptions, validationOptions: { checkPaths: options.jsonpathValidation, checkArn: options.arnValidation, + noValidate: !options.validation, }, }); } catch (error) { diff --git a/src/typings/CLI.ts b/src/typings/CLI.ts index b223871..57b3278 100644 --- a/src/typings/CLI.ts +++ b/src/typings/CLI.ts @@ -11,6 +11,7 @@ export type ParsedCommandOptions = { contextFile: Context; jsonpathValidation: boolean; arnValidation: boolean; + validation: boolean; }; export enum ExitCodes { From 1bafee10ac90e66e08e6bb5da09143baafd16cb8 Mon Sep 17 00:00:00 2001 From: Luis De Anda Date: Sun, 17 Sep 2023 14:02:28 -0600 Subject: [PATCH 5/7] Add JSDoc comments to constructor and run options --- src/typings/StateMachineImplementation.ts | 52 ++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/typings/StateMachineImplementation.ts b/src/typings/StateMachineImplementation.ts index 0b940e9..fc79c32 100644 --- a/src/typings/StateMachineImplementation.ts +++ b/src/typings/StateMachineImplementation.ts @@ -13,33 +13,83 @@ export type WaitStateTimeOverride = { }; interface Overrides { + /** + * Pass an object to this option to override a `Task` state to run a local function, + * instead of calling the service specified in the `Resource` field. + */ taskResourceLocalHandlers?: TaskStateResourceLocalHandler; + + /** + * Pass an object to this option to override a `Wait` state to pause for the specified number of milliseconds, + * instead of pausing for the duration specified by the `Seconds`, `Timestamp`, `SecondsPath`, or `TimestampPath` fields. + */ waitTimeOverrides?: WaitStateTimeOverride; } export interface ValidationOptions { + /** + * Disables validation of the state machine definition entirely. + * + * 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. + */ readonly noValidate?: boolean; + + /** + * Disables validation of JSONPath expressions in the state machine definition. + */ readonly checkPaths?: boolean; + + /** + * Disables validation of ARNs in the state machine definition. + */ readonly checkArn?: boolean; } export interface AWSConfig { + /** + * AWS Region where your Task resources are located. + */ region: string; + + /** + * AWS credentials needed to be able to call Task resources. + */ credentials?: { cognitoIdentityPool?: FromCognitoIdentityPoolParameters; - accessKeys?: Omit; + accessKeys?: AWSCredentials; }; } export interface StateMachineOptions { + /** + * Options that allow changing certain rules when validating the state machine definition. + */ validationOptions?: ValidationOptions; + + /** + * Config options related to AWS resources. + */ awsConfig?: AWSConfig; } export interface RunOptions { + /** + * This option allows overriding the behavior of certain states. + */ overrides?: Overrides; + + /** + * If set to `true`, aborting the execution will simply return `null` as result instead of throwing + */ noThrowOnAbort?: boolean; + + /** + * Pass an object to this option to mock the [Context Object](https://states-language.net/#context-object) that will be used in the execution. + * @see https://docs.aws.amazon.com/step-functions/latest/dg/input-output-contextobject.html + */ context?: Context; + /** * @internal DO NOT USE. * From d5d965f1bd057fa81fc2337322b7a0734dbba82e Mon Sep 17 00:00:00 2001 From: Luis De Anda Date: Sun, 17 Sep 2023 18:48:17 -0600 Subject: [PATCH 6/7] Move explanation of `StateMachine.run` return value to its own header section --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a1d7ec4..4ebcef9 100644 --- a/README.md +++ b/README.md @@ -138,11 +138,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. @@ -156,6 +152,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 From 05ba0ac244acb33d4134e058be3dbd974370a252 Mon Sep 17 00:00:00 2001 From: Luis De Anda Date: Sun, 17 Sep 2023 21:39:55 -0600 Subject: [PATCH 7/7] Add list of use cases --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 4ebcef9..59f469e 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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