diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/condition.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/condition.ts index bc264874f8093..12413b1335ea0 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/condition.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/condition.ts @@ -338,6 +338,13 @@ export abstract class Condition { return new NotCondition(condition); } + /** + * JSONata expression condition + */ + public static jsonata(conditon: string): Condition { + return new JsonataCondition(conditon); + } + /** * Render Amazon States Language JSON for the condition */ @@ -452,3 +459,21 @@ class NotCondition extends Condition { }; } } + +/** + * JSONata for Condition + */ +class JsonataCondition extends Condition { + constructor(private readonly condition: string) { + super(); + if (!/^{%(.*)%}$/.test(condition)) { + throw new Error(`Variable reference must be '$', start with '$.', or start with '$[', got '${condition}'`); + } + } + + public renderCondition(): any { + return { + Condition: this.condition, + }; + } +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/state-graph.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/state-graph.ts index 364b43064883a..6444b0e0a1615 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/state-graph.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/state-graph.ts @@ -1,6 +1,7 @@ import { StateMachine } from './state-machine'; import { DistributedMap } from './states/distributed-map'; import { State } from './states/state'; +import { QueryLanguage } from './types'; import * as iam from '../../aws-iam'; import { Duration } from '../../core'; @@ -105,10 +106,10 @@ export class StateGraph { /** * Return the Amazon States Language JSON for this graph */ - public toGraphJson(): object { + public toGraphJson(queryLanguage?: QueryLanguage): object { const states: any = {}; for (const state of this.allStates) { - states[state.stateId] = state.toStateJson(); + states[state.stateId] = state.toStateJson(queryLanguage); } return { diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/state-machine.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/state-machine.ts index 5fea9d8ab09e4..a800c7b3a89a0 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/state-machine.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/state-machine.ts @@ -5,7 +5,7 @@ import { buildEncryptionConfiguration } from './private/util'; import { StateGraph } from './state-graph'; import { StatesMetrics } from './stepfunctions-canned-metrics.generated'; import { CfnStateMachine } from './stepfunctions.generated'; -import { IChainable } from './types'; +import { IChainable, QueryLanguage } from './types'; import * as cloudwatch from '../../aws-cloudwatch'; import * as iam from '../../aws-iam'; import * as logs from '../../aws-logs'; @@ -129,6 +129,15 @@ export interface StateMachineProps { */ readonly comment?: string; + /** + * The name of the query language used by the state machine. + * If the state does not contain a `queryLanguage` field, + * then it will use the query language specified in the top-level `queryLanguage` field. + * + * @default - JSONPATH + */ + readonly queryLanguage?: QueryLanguage; + /** * Type of the state machine * @@ -786,9 +795,13 @@ export class ChainDefinitionBody extends DefinitionBody { } public bind(scope: Construct, _sfnPrincipal: iam.IPrincipal, sfnProps: StateMachineProps, graph?: StateGraph): DefinitionConfig { - const graphJson = graph!.toGraphJson(); + const graphJson = graph!.toGraphJson(sfnProps.queryLanguage); return { - definitionString: Stack.of(scope).toJsonString({ ...graphJson, Comment: sfnProps.comment }), + definitionString: Stack.of(scope).toJsonString({ + ...graphJson, + Comment: sfnProps.comment, + QueryLanguage: sfnProps.queryLanguage, + }), }; } } diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/choice.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/choice.ts index 1b35552b08a0b..b5bdafca753c9 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/choice.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/choice.ts @@ -1,48 +1,28 @@ import { Construct } from 'constructs'; import { StateType } from './private/state-type'; -import { ChoiceTransitionOptions, State } from './state'; +import { ChoiceTransitionOptions, JsonataCommonOptions, JsonPathCommonOptions, State, StateBaseProps } from './state'; import { Chain } from '../chain'; import { Condition } from '../condition'; -import { IChainable, INextable } from '../types'; +import { IChainable, INextable, QueryLanguage } from '../types'; + +interface ChoiceBaseProps extends StateBaseProps {} +interface ChoiceJsonPathOptions extends JsonPathCommonOptions {} +interface ChoiceJsonataOptions extends JsonataCommonOptions {} /** - * Properties for defining a Choice state + * Properties for defining a Choice state that using JSONPath */ -export interface ChoiceProps { - /** - * Optional name for this state - * - * @default - The construct ID will be used as state name - */ - readonly stateName?: string; - - /** - * An optional description for this state - * - * @default No comment - */ - readonly comment?: string; +export interface ChoiceJsonPathProps extends ChoiceBaseProps, ChoiceJsonPathOptions { } - /** - * JSONPath expression to select part of the state to be the input to this state. - * - * May also be the special value JsonPath.DISCARD, which will cause the effective - * input to be the empty object {}. - * - * @default $ - */ - readonly inputPath?: string; +/** + * Properties for defining a Choice state that using JSONata + */ +export interface ChoiceJsonataProps extends ChoiceBaseProps, ChoiceJsonataOptions { } - /** - * JSONPath expression to select part of the state to be the output to this state. - * - * May also be the special value JsonPath.DISCARD, which will cause the effective - * output to be the empty object {}. - * - * @default $ - */ - readonly outputPath?: string; -} +/** + * Properties for defining a Choice state + */ +export interface ChoiceProps extends ChoiceBaseProps, ChoiceJsonPathOptions, ChoiceJsonataOptions { } /** * Define a Choice in the state machine @@ -51,6 +31,27 @@ export interface ChoiceProps { * state. */ export class Choice extends State { + /** + * Define a Choice using JSONPath in the state machine + * + * A choice state can be used to make decisions based on the execution + * state. + */ + public static jsonPath(scope: Construct, id: string, props: ChoiceJsonPathProps = {}) { + return new Choice(scope, id, props); + } + /** + * Define a Choice using JSONata in the state machine + * + * A choice state can be used to make decisions based on the execution + * state. + */ + public static jsonata(scope: Construct, id: string, props: ChoiceJsonataProps = {}) { + return new Choice(scope, id, { + ...props, + queryLanguage: QueryLanguage.JSONATA, + }); + } public readonly endStates: INextable[] = []; constructor(scope: Construct, id: string, props: ChoiceProps = {}) { @@ -95,9 +96,10 @@ export class Choice extends State { /** * Return the Amazon States Language object for this state */ - public toStateJson(): object { + public toStateJson(queryLanguage?: QueryLanguage): object { return { Type: StateType.CHOICE, + ...this.renderQueryLanguage(queryLanguage), Comment: this.comment, ...this.renderInputOutput(), ...this.renderChoices(), diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/custom-state.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/custom-state.ts index c8a96a4d09f16..4aa5bd7ed86fd 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/custom-state.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/custom-state.ts @@ -2,7 +2,7 @@ import { Construct } from 'constructs'; import { Chain } from '..'; import { State } from './state'; import { Annotations } from '../../../core/'; -import { CatchProps, IChainable, INextable, RetryProps } from '../types'; +import { CatchProps, IChainable, INextable, QueryLanguage, RetryProps } from '../types'; /** * Properties for defining a custom state definition @@ -68,8 +68,9 @@ export class CustomState extends State implements IChainable, INextable { /** * Returns the Amazon States Language object for this state */ - public toStateJson(): object { + public toStateJson(queryLanguage?: QueryLanguage): object { const state = { + ...this.renderQueryLanguage(queryLanguage), ...this.renderNextEnd(), ...this.stateJson, ...this.renderRetryCatch(), diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/distributed-map.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/distributed-map.ts index e5d360f8e24ff..cb45202378d8c 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/distributed-map.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/distributed-map.ts @@ -2,19 +2,16 @@ import { Construct } from 'constructs'; import { ItemBatcher } from './distributed-map/item-batcher'; import { IItemReader } from './distributed-map/item-reader'; import { ResultWriter } from './distributed-map/result-writer'; -import { MapBase, MapBaseProps } from './map-base'; +import { MapBase, MapBaseJsonataOptions, MapBaseJsonPathOptions, MapBaseOptions } from './map-base'; import { Annotations } from '../../../core'; import { FieldUtils } from '../fields'; import { StateGraph } from '../state-graph'; import { StateMachineType } from '../state-machine'; -import { CatchProps, IChainable, INextable, ProcessorConfig, ProcessorMode, RetryProps } from '../types'; +import { CatchProps, IChainable, INextable, ProcessorConfig, ProcessorMode, QueryLanguage, RetryProps } from '../types'; const DISTRIBUTED_MAP_SYMBOL = Symbol.for('@aws-cdk/aws-stepfunctions.DistributedMap'); -/** - * Properties for configuring a Distribute Map state - */ -export interface DistributedMapProps extends MapBaseProps { +interface DistributedMapBaseProps extends MapBaseOptions { /** * MapExecutionType * @@ -44,15 +41,6 @@ export interface DistributedMapProps extends MapBaseProps { */ readonly toleratedFailurePercentage?: number; - /** - * ToleratedFailurePercentagePath - * - * Percentage of failed items to tolerate in a Map Run, as JsonPath - * - * @default - No toleratedFailurePercentagePath - */ - readonly toleratedFailurePercentagePath?: string; - /** * ToleratedFailureCount * @@ -62,15 +50,6 @@ export interface DistributedMapProps extends MapBaseProps { */ readonly toleratedFailureCount?: number; - /** - * ToleratedFailureCountPath - * - * Number of failed items to tolerate in a Map Run, as JsonPath - * - * @default - No toleratedFailureCountPath - */ - readonly toleratedFailureCountPath?: string; - /** * Label * @@ -95,6 +74,43 @@ export interface DistributedMapProps extends MapBaseProps { readonly itemBatcher?: ItemBatcher; } +interface DistributedMapJsonPathOptions extends MapBaseJsonPathOptions { + /** + * ToleratedFailurePercentagePath + * + * Percentage of failed items to tolerate in a Map Run, as JsonPath + * + * @default - No toleratedFailurePercentagePath + */ + readonly toleratedFailurePercentagePath?: string; + + /** + * ToleratedFailureCountPath + * + * Number of failed items to tolerate in a Map Run, as JsonPath + * + * @default - No toleratedFailureCountPath + */ + readonly toleratedFailureCountPath?: string; +} + +interface DistributedMapJsonataOptions extends MapBaseJsonataOptions {} + +/** + * Properties for configuring a Distribute Map state that using JSONPath + */ +export interface DistributedMapJsonPathProps extends DistributedMapBaseProps, DistributedMapJsonPathOptions {} + +/** + * Properties for configuring a Distribute Map state that using JSONata + */ +export interface DistributedMapJsonataProps extends DistributedMapBaseProps, DistributedMapJsonataOptions {} + +/** + * Properties for configuring a Distribute Map state + */ +export interface DistributedMapProps extends DistributedMapBaseProps, DistributedMapJsonPathOptions, DistributedMapJsonataOptions {} + /** * Define a Distributed Mode Map state in the state machine * @@ -109,6 +125,41 @@ export interface DistributedMapProps extends MapBaseProps { * @see https://docs.aws.amazon.com/step-functions/latest/dg/concepts-asl-use-map-state-distributed.html */ export class DistributedMap extends MapBase implements INextable { + /** + * Define a Distributed Mode Map state using JSONPath in the state machine + * + * A `Map` state can be used to run a set of steps for each element of an input array. + * A Map state will execute the same steps for multiple entries of an array in the state input. + * + * While the Parallel state executes multiple branches of steps using the same input, a Map state + * will execute the same steps for multiple entries of an array in the state input. + * + * A `Map` state in `Distributed` mode will execute a child workflow for each iteration of the Map state. + * This serves to increase concurrency and allows for larger workloads to be run in a single state machine. + * @see https://docs.aws.amazon.com/step-functions/latest/dg/concepts-asl-use-map-state-distributed.html + */ + public static jsonPath(scope: Construct, id: string, props: DistributedMapJsonPathProps = {}) { + return new DistributedMap(scope, id, props); + } + /** + * Define a Distributed Mode Map state using JSONata in the state machine + * + * A `Map` state can be used to run a set of steps for each element of an input array. + * A Map state will execute the same steps for multiple entries of an array in the state input. + * + * While the Parallel state executes multiple branches of steps using the same input, a Map state + * will execute the same steps for multiple entries of an array in the state input. + * + * A `Map` state in `Distributed` mode will execute a child workflow for each iteration of the Map state. + * This serves to increase concurrency and allows for larger workloads to be run in a single state machine. + * @see https://docs.aws.amazon.com/step-functions/latest/dg/concepts-asl-use-map-state-distributed.html + */ + public static jsonata(scope: Construct, id: string, props: DistributedMapJsonataProps = {}) { + return new DistributedMap(scope, id, { + ...props, + queryLanguage: QueryLanguage.JSONATA, + }); + } /** * Return whether the given object is a DistributedMap. */ @@ -239,8 +290,8 @@ export class DistributedMap extends MapBase implements INextable { /** * Return the Amazon States Language object for this state */ - public toStateJson(): object { - let rendered: any = super.toStateJson(); + public toStateJson(stateMachineQueryLanguage?: QueryLanguage): object { + let rendered: any = super.toStateJson(stateMachineQueryLanguage); if (rendered.ItemProcessor.ProcessorConfig.ExecutionType) { Annotations.of(this).addWarningV2('@aws-cdk/aws-stepfunctions:propertyIgnored', 'Property \'ProcessorConfig.executionType\' is ignored, use the \'mapExecutionType\' in the \'DistributedMap\' class instead.'); } diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/fail.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/fail.ts index 13bc5c702d2a8..de290a71faa84 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/fail.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/fail.ts @@ -1,34 +1,26 @@ import { Construct } from 'constructs'; import { StateType } from './private/state-type'; -import { renderJsonPath, State } from './state'; +import { renderJsonPath, State, StateBaseProps } from './state'; import { Token } from '../../../core'; -import { INextable } from '../types'; +import { INextable, QueryLanguage } from '../types'; -/** - * Properties for defining a Fail state - */ -export interface FailProps { +interface FailBaseProps extends StateBaseProps { /** - * Optional name for this state - * - * @default - The construct ID will be used as state name - */ - readonly stateName?: string; - - /** - * An optional description for this state + * Error code used to represent this failure * - * @default - No comment + * @default - No error code */ - readonly comment?: string; + readonly error?: string; /** - * Error code used to represent this failure + * A description for the cause of the failure * - * @default - No error code + * @default - No description */ - readonly error?: string; + readonly cause?: string; +} +interface FailJsonPathOptions { /** * JsonPath expression to select part of the state to be the error to this state. * @@ -39,13 +31,6 @@ export interface FailProps { */ readonly errorPath?: string; - /** - * A description for the cause of the failure - * - * @default - No description - */ - readonly cause?: string; - /** * JsonPath expression to select part of the state to be the cause to this state. * @@ -56,6 +41,20 @@ export interface FailProps { */ readonly causePath?: string; } +interface FailJsonataOptions { } + +/** + * Properties for defining a Fail state that using JSONPath + */ +export interface FailJsonPathProps extends FailBaseProps, FailJsonPathOptions { } +/** + * Properties for defining a Fail state that using JSONata + */ +export interface FailJsonataProps extends FailBaseProps, FailJsonataOptions { } +/** + * Properties for defining a Fail state + */ +export interface FailProps extends FailBaseProps, FailJsonPathOptions, FailJsonataOptions { } /** * Define a Fail state in the state machine @@ -63,6 +62,25 @@ export interface FailProps { * Reaching a Fail state terminates the state execution in failure. */ export class Fail extends State { +/** + * Define a Fail state using JSONPath in the state machine + * + * Reaching a Fail state terminates the state execution in failure. + */ + public static jsonPath(scope: Construct, id: string, props: FailJsonPathProps = {}) { + return new Fail(scope, id, props); + } + /** + * Define a Fail state using JSONata in the state machine + * + * Reaching a Fail state terminates the state execution in failure. + */ + public static jsonata(scope: Construct, id: string, props: FailJsonataProps = {}) { + return new Fail(scope, id, { + ...props, + queryLanguage: QueryLanguage.JSONATA, + }); + } private static allowedIntrinsics = [ 'States.Format', 'States.JsonToString', @@ -92,9 +110,10 @@ export class Fail extends State { /** * Return the Amazon States Language object for this state */ - public toStateJson(): object { + public toStateJson(queryLanguage?: QueryLanguage): object { return { Type: StateType.FAIL, + ...this.renderQueryLanguage(queryLanguage), Comment: this.comment, Error: this.error, ErrorPath: this.isIntrinsicString(this.errorPath) ? this.errorPath : renderJsonPath(this.errorPath), diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/map-base.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/map-base.ts index 2359558b33624..1fe1378592597 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/map-base.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/map-base.ts @@ -1,48 +1,33 @@ import { Construct } from 'constructs'; import { StateType } from './private/state-type'; -import { renderJsonPath, State } from './state'; +import { JsonataCommonOptions, JsonPathCommonOptions, renderJsonPath, State, StateBaseProps } from './state'; import { Token } from '../../../core'; import { Chain } from '../chain'; import { FieldUtils } from '../fields'; -import { IChainable, INextable, ProcessorMode } from '../types'; +import { IChainable, INextable, ProcessorMode, QueryLanguage } from '../types'; /** - * Properties for defining a Map state + * Base properties for defining a Map state that using JSONPath */ -export interface MapBaseProps { - /** - * Optional name for this state - * - * @default - The construct ID will be used as state name - */ - readonly stateName?: string; - - /** - * An optional description for this state - * - * @default No comment - */ - readonly comment?: string; - +export interface MapBaseJsonPathOptions extends JsonPathCommonOptions { /** - * JSONPath expression to select part of the state to be the input to this state. - * - * May also be the special value JsonPath.DISCARD, which will cause the effective - * input to be the empty object {}. + * JSONPath expression to select the array to iterate over * * @default $ */ - readonly inputPath?: string; + readonly itemsPath?: string; /** - * JSONPath expression to select part of the state to be the output to this state. + * MaxConcurrencyPath * - * May also be the special value JsonPath.DISCARD, which will cause the effective - * output to be the empty object {}. + * A JsonPath that specifies the maximum concurrency dynamically from the state input. * - * @default $ + * @see + * https://docs.aws.amazon.com/step-functions/latest/dg/concepts-asl-use-map-state-inline.html#map-state-inline-additional-fields + * + * @default - full concurrency */ - readonly outputPath?: string; + readonly maxConcurrencyPath?: string; /** * JSONPath expression to indicate where to inject the state's output @@ -54,23 +39,6 @@ export interface MapBaseProps { */ readonly resultPath?: string; - /** - * JSONPath expression to select the array to iterate over - * - * @default $ - */ - readonly itemsPath?: string; - - /** - * The JSON that you want to override your default iteration input (mutually exclusive with `parameters`). - * - * @see - * https://docs.aws.amazon.com/step-functions/latest/dg/input-output-itemselector.html - * - * @default $ - */ - readonly itemSelector?: { [key: string]: any }; - /** * The JSON that will replace the state's raw result and become the effective * result before ResultPath is applied. @@ -84,7 +52,45 @@ export interface MapBaseProps { * @default - None */ readonly resultSelector?: { [key: string]: any }; +} + +/** + * The array that the Map state will iterate over. + */ +export abstract class ProvideItems { + /** + * Use a JSON array as Map state items. + * + * Example value: `[1, "{% $two %}", 3]` + */ + public static jsonArray(array: any[]): ProvideItems { return { items: array }; } + /** + * Use a JSONata expression as Map state items. + * + * Example value: `{% $states.input.items %}` + */ + public static jsonata(jsonataExpression: string): ProvideItems { return { items: jsonataExpression }; } + /** + * The array that the Map state will iterate over. + */ + public abstract readonly items: any; +} + +/** + * Base properties for defining a Map state that using JSONata + */ +export interface MapBaseJsonataOptions extends JsonataCommonOptions { + /** + * The array that the Map state will iterate over. + * @default - The state input as is. + */ + readonly items?: ProvideItems; +} +/** + * Base properties for defining a Map state + */ +export interface MapBaseOptions extends StateBaseProps { /** * MaxConcurrency * @@ -98,18 +104,21 @@ export interface MapBaseProps { readonly maxConcurrency?: number; /** - * MaxConcurrencyPath - * - * A JsonPath that specifies the maximum concurrency dynamically from the state input. + * The JSON that you want to override your default iteration input (mutually exclusive with `parameters`). * * @see - * https://docs.aws.amazon.com/step-functions/latest/dg/concepts-asl-use-map-state-inline.html#map-state-inline-additional-fields + * https://docs.aws.amazon.com/step-functions/latest/dg/input-output-itemselector.html * - * @default - full concurrency + * @default $ */ - readonly maxConcurrencyPath?: string; + readonly itemSelector?: { [key: string]: any }; } +/** + * Properties for defining a Map state + */ +export interface MapBaseProps extends MapBaseOptions, MapBaseJsonPathOptions, MapBaseJsonataOptions {} + /** * Returns true if the value passed is a positive integer * @param value the value to validate @@ -138,6 +147,7 @@ export abstract class MapBase extends State implements INextable { private readonly maxConcurrency?: number; private readonly maxConcurrencyPath?: string; + protected readonly items?: ProvideItems; protected readonly itemsPath?: string; protected readonly itemSelector?: { [key: string]: any }; @@ -146,6 +156,7 @@ export abstract class MapBase extends State implements INextable { this.endStates = [this]; this.maxConcurrency = props.maxConcurrency; this.maxConcurrencyPath = props.maxConcurrencyPath; + this.items = props.items; this.itemsPath = props.itemsPath; this.itemSelector = props.itemSelector; } @@ -161,15 +172,17 @@ export abstract class MapBase extends State implements INextable { /** * Return the Amazon States Language object for this state */ - public toStateJson(): object { + public toStateJson(stateMachineQueryLanguage?: QueryLanguage): object { return { Type: StateType.MAP, + ...this.renderQueryLanguage(stateMachineQueryLanguage), Comment: this.comment, ResultPath: renderJsonPath(this.resultPath), ...this.renderNextEnd(), ...this.renderInputOutput(), ...this.renderResultSelector(), ...this.renderRetryCatch(), + Items: this.items?.items, ...this.renderItemsPath(), ...this.renderItemSelector(), ...this.renderItemProcessor(), diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/map.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/map.ts index de7cfa3fa60df..4a6185e1ff91c 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/map.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/map.ts @@ -1,13 +1,10 @@ import { Construct } from 'constructs'; -import { MapBase, MapBaseProps } from './map-base'; +import { MapBase, MapBaseJsonataOptions, MapBaseJsonPathOptions, MapBaseOptions, MapBaseProps } from './map-base'; import { FieldUtils } from '../fields'; import { StateGraph } from '../state-graph'; -import { CatchProps, IChainable, INextable, ProcessorConfig, ProcessorMode, RetryProps } from '../types'; +import { CatchProps, IChainable, INextable, ProcessorConfig, ProcessorMode, QueryLanguage, RetryProps } from '../types'; -/** - * Properties for defining a Map state - */ -export interface MapProps extends MapBaseProps { +interface MapOptions { /** * The JSON that you want to override your default iteration input (mutually exclusive with `itemSelector`). * @@ -21,6 +18,20 @@ export interface MapProps extends MapBaseProps { */ readonly parameters?: { [key: string]: any }; } +/** + * Properties for defining a Map state that using JSONPath + */ +export interface MapJsonPathProps extends MapBaseOptions, MapOptions, MapBaseJsonPathOptions {} + +/** + * Properties for defining a Map state that using JSONata + */ +export interface MapJsonataProps extends MapBaseOptions, MapOptions, MapBaseJsonataOptions {} + +/** + * Properties for defining a Map state + */ +export interface MapProps extends MapBaseProps, MapOptions {} /** * Define a Map state in the state machine @@ -34,6 +45,37 @@ export interface MapProps extends MapBaseProps { * @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-map-state.html */ export class Map extends MapBase implements INextable { + /** + * Define a Map state using JSONPath in the state machine + * + * A `Map` state can be used to run a set of steps for each element of an input array. + * A Map state will execute the same steps for multiple entries of an array in the state input. + * + * While the Parallel state executes multiple branches of steps using the same input, a Map state + * will execute the same steps for multiple entries of an array in the state input. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-map-state.html + */ + public static jsonPath(scope: Construct, id: string, props: MapJsonPathProps = {}) { + return new Map(scope, id, props); + } + /** + * Define a Map state using JSONata in the state machine + * + * A `Map` state can be used to run a set of steps for each element of an input array. + * A Map state will execute the same steps for multiple entries of an array in the state input. + * + * While the Parallel state executes multiple branches of steps using the same input, a Map state + * will execute the same steps for multiple entries of an array in the state input. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-map-state.html + */ + public static jsonata(scope: Construct, id: string, props: MapJsonataProps = {}) { + return new Map(scope, id, { + ...props, + queryLanguage: QueryLanguage.JSONATA, + }); + } constructor(scope: Construct, id: string, props: MapProps = {}) { super(scope, id, props); this.processorMode = ProcessorMode.INLINE; @@ -55,9 +97,9 @@ export class Map extends MapBase implements INextable { /** * Return the Amazon States Language object for this state */ - public toStateJson(): object { + public toStateJson(queryLanguage?: QueryLanguage): object { return { - ...super.toStateJson(), + ...super.toStateJson(queryLanguage), ...this.renderParameters(), ...this.renderIterator(), }; diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/parallel.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/parallel.ts index 1199f3d84e8b7..ef7f2db3f2fe0 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/parallel.ts @@ -1,48 +1,12 @@ import { Construct } from 'constructs'; import { StateType } from './private/state-type'; -import { renderJsonPath, State } from './state'; +import { JsonataCommonOptions, JsonPathCommonOptions, renderJsonPath, State, StateBaseProps } from './state'; import { Chain } from '../chain'; import { StateGraph } from '../state-graph'; -import { CatchProps, IChainable, INextable, RetryProps } from '../types'; - -/** - * Properties for defining a Parallel state - */ -export interface ParallelProps { - /** - * Optional name for this state - * - * @default - The construct ID will be used as state name - */ - readonly stateName?: string; - - /** - * An optional description for this state - * - * @default No comment - */ - readonly comment?: string; - - /** - * JSONPath expression to select part of the state to be the input to this state. - * - * May also be the special value JsonPath.DISCARD, which will cause the effective - * input to be the empty object {}. - * - * @default $ - */ - readonly inputPath?: string; - - /** - * JSONPath expression to select part of the state to be the output to this state. - * - * May also be the special value JsonPath.DISCARD, which will cause the effective - * output to be the empty object {}. - * - * @default $ - */ - readonly outputPath?: string; +import { CatchProps, IChainable, INextable, QueryLanguage, RetryProps } from '../types'; +interface ParallelBaseProps extends StateBaseProps {} +interface ParallelJsonPathOptions extends JsonPathCommonOptions { /** * JSONPath expression to indicate where to inject the state's output * @@ -67,6 +31,32 @@ export interface ParallelProps { */ readonly resultSelector?: { [key: string]: any }; } +interface ParallelJsonataOptions extends JsonataCommonOptions { + /** + * Parameters pass a collection of key-value pairs, either static values or JSONata expressions that select from the input. + * + * @see + * https://docs.aws.amazon.com/step-functions/latest/dg/transforming-data.html + * + * @default No arguments + */ + readonly arguments?: { [name: string]: any }; +} + +/** + * Properties for defining a Parallel state that using JSONPath + */ +export interface ParallelJsonPathProps extends ParallelBaseProps, ParallelJsonPathOptions {} + +/** + * Properties for defining a Parallel state that using JSONata + */ +export interface ParallelJsonataProps extends ParallelBaseProps, ParallelJsonataOptions {} + +/** + * Properties for defining a Parallel state + */ +export interface ParallelProps extends ParallelBaseProps, ParallelJsonPathOptions, ParallelJsonataOptions {} /** * Define a Parallel state in the state machine @@ -77,6 +67,31 @@ export interface ParallelProps { * The Result of a Parallel state is an array of the results of its substatemachines. */ export class Parallel extends State implements INextable { + /** + * Define a Parallel state using JSONPath in the state machine + * + * A Parallel state can be used to run one or more state machines at the same + * time. + * + * The Result of a Parallel state is an array of the results of its substatemachines. + */ + public static jsonPath(scope: Construct, id: string, props: ParallelJsonPathProps = {}) { + return new Parallel(scope, id, props); + } + /** + * Define a Parallel state using JSONata in the state machine + * + * A Parallel state can be used to run one or more state machines at the same + * time. + * + * The Result of a Parallel state is an array of the results of its substatemachines. + */ + public static jsonata(scope: Construct, id: string, props: ParallelJsonataProps = {}) { + return new Parallel(scope, id, { + ...props, + queryLanguage: QueryLanguage.JSONATA, + }); + } public readonly endStates: INextable[]; private readonly _branches: IChainable[] = []; @@ -142,9 +157,10 @@ export class Parallel extends State implements INextable { /** * Return the Amazon States Language object for this state */ - public toStateJson(): object { + public toStateJson(queryLanguage?: QueryLanguage): object { return { Type: StateType.PARALLEL, + ...this.renderQueryLanguage(queryLanguage), Comment: this.comment, ResultPath: renderJsonPath(this.resultPath), ...this.renderNextEnd(), diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/pass.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/pass.ts index 2a2a19db7d47c..4b408e7efd5fd 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/pass.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/pass.ts @@ -1,9 +1,9 @@ import { Construct } from 'constructs'; import { StateType } from './private/state-type'; -import { renderJsonPath, State } from './state'; +import { JsonataCommonOptions, renderJsonPath, State, StateBaseProps } from './state'; import { Chain } from '../chain'; import { FieldUtils } from '../fields'; -import { IChainable, INextable } from '../types'; +import { IChainable, INextable, QueryLanguage } from '../types'; /** * The result of a Pass operation @@ -52,24 +52,8 @@ export class Result { } } -/** - * Properties for defining a Pass state - */ -export interface PassProps { - /** - * Optional name for this state - * - * @default - The construct ID will be used as state name - */ - readonly stateName?: string; - - /** - * An optional description for this state - * - * @default No comment - */ - readonly comment?: string; - +interface PassBaseProps extends StateBaseProps {} +interface PassJsonPathOptions { /** * JSONPath expression to select part of the state to be the input to this state. * @@ -119,6 +103,22 @@ export interface PassProps { */ readonly parameters?: { [name: string]: any }; } +interface PassJsonataOptions extends JsonataCommonOptions {} + +/** + * Properties for defining a Pass state that using JSONPath + */ +export interface PassJsonPathProps extends PassBaseProps, PassJsonPathOptions {} + +/** + * Properties for defining a Pass state that using JSONata + */ +export interface PassJsonataProps extends PassBaseProps, PassJsonataOptions {} + +/** + * Properties for defining a Pass state + */ +export interface PassProps extends PassBaseProps, PassJsonPathOptions, PassJsonataOptions {} /** * Define a Pass in the state machine @@ -126,6 +126,25 @@ export interface PassProps { * A Pass state can be used to transform the current execution's state. */ export class Pass extends State implements INextable { + /** + * Define a Pass using JSONPath in the state machine + * + * A Pass state can be used to transform the current execution's state. + */ + public static jsonPath(scope: Construct, id: string, props: PassJsonPathProps = {}) { + return new Pass(scope, id, props); + } + /** + * Define a Pass using JSONata in the state machine + * + * A Pass state can be used to transform the current execution's state. + */ + public static jsonata(scope: Construct, id: string, props: PassJsonataProps = {}) { + return new Pass(scope, id, { + ...props, + queryLanguage: QueryLanguage.JSONATA, + }); + } public readonly endStates: INextable[]; private readonly result?: Result; @@ -148,9 +167,10 @@ export class Pass extends State implements INextable { /** * Return the Amazon States Language object for this state */ - public toStateJson(): object { + public toStateJson(queryLanguage?: QueryLanguage): object { return { Type: StateType.PASS, + ...this.renderQueryLanguage(queryLanguage), Comment: this.comment, Result: this.result?.value, ResultPath: renderJsonPath(this.resultPath), diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/state.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/state.ts index 0ef1e394a29ee..a2c2e45369368 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/state.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/state.ts @@ -3,12 +3,21 @@ import { Token } from '../../../core'; import { Condition } from '../condition'; import { FieldUtils } from '../fields'; import { StateGraph } from '../state-graph'; -import { CatchProps, Errors, IChainable, INextable, ProcessorConfig, ProcessorMode, RetryProps } from '../types'; +import { CatchProps, Errors, IChainable, INextable, ProcessorConfig, ProcessorMode, QueryLanguage, RetryProps } from '../types'; /** * Properties shared by all states */ -export interface StateProps { +export interface StateBaseProps { + /** + * The name of the query language used by the state. + * If the state does not contain a `queryLanguage` field, + * then it will use the query language specified in the top-level `queryLanguage` field. + * + * @default - JSONPath + */ + readonly queryLanguage?: QueryLanguage; + /** * Optional name for this state * @@ -22,7 +31,12 @@ export interface StateProps { * @default No comment */ readonly comment?: string; +} +/** + * Option properties for JSONPath state. + */ +export interface JsonPathCommonOptions { /** * JSONPath expression to select part of the state to be the input to this state. * @@ -33,16 +47,6 @@ export interface StateProps { */ readonly inputPath?: string; - /** - * Parameters pass a collection of key-value pairs, either static values or JSONPath expressions that select from the input. - * - * @see - * https://docs.aws.amazon.com/step-functions/latest/dg/input-output-inputpath-params.html#input-output-parameters - * - * @default No parameters - */ - readonly parameters?: { [name: string]: any }; - /** * JSONPath expression to select part of the state to be the output to this state. * @@ -52,7 +56,9 @@ export interface StateProps { * @default $ */ readonly outputPath?: string; +} +interface JsonPathStateOptions extends JsonPathCommonOptions { /** * JSONPath expression to indicate where to inject the state's output * @@ -76,8 +82,67 @@ export interface StateProps { * @default - None */ readonly resultSelector?: { [key: string]: any }; + + /** + * Parameters pass a collection of key-value pairs, either static values or JSONPath expressions that select from the input. + * + * @see + * https://docs.aws.amazon.com/step-functions/latest/dg/input-output-inputpath-params.html#input-output-parameters + * + * @default No parameters + */ + readonly parameters?: { [name: string]: any }; +} + +/** + * Option properties for JSONata state. + */ +export interface JsonataCommonOptions { + /** + * Used to specify and transform output from the state. + * When specified, the value overrides the state output default. + * The output field accepts any JSON value (object, array, string, number, boolean, null). + * Any string value, including those inside objects or arrays, + * will be evaluated as JSONata if surrounded by {% %} characters. + * Output also accepts a JSONata expression directly. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/concepts-input-output-filtering.html + * + * @default - None + */ + readonly output?: any; +} + +/** + * Option properties for JSONata task state. + */ +export interface JsonataStateOptions extends JsonataCommonOptions { + /** + * Parameters pass a collection of key-value pairs, either static values or JSONata expressions that select from the input. + * + * @see + * https://docs.aws.amazon.com/step-functions/latest/dg/transforming-data.html + * + * @default - No arguments + */ + readonly arguments?: { [name: string]: any }; } +/** + * Properties shared by all states that use JSONPath + */ +export interface JsonPathStateProps extends StateBaseProps, JsonPathStateOptions {} + +/** + * Properties shared by all states that use JSONata + */ +export interface JsonataStateProps extends StateBaseProps, JsonataStateOptions {} + +/** + * Properties shared by all states + */ +export interface StateProps extends StateBaseProps, JsonPathStateOptions, JsonataStateOptions {} + /** * Base class for all other state classes */ @@ -170,6 +235,9 @@ export abstract class State extends Construct implements IChainable { protected readonly resultPath?: string; protected readonly resultSelector?: object; protected readonly branches: StateGraph[] = []; + protected readonly queryLanguage?: QueryLanguage; + protected readonly output?: object; + protected readonly arguments?: object; protected iteration?: StateGraph; protected processorMode?: ProcessorMode = ProcessorMode.INLINE; protected processor?: StateGraph; @@ -206,12 +274,15 @@ export abstract class State extends Construct implements IChainable { this.startState = this; this.stateName = props.stateName; + this.queryLanguage = props.queryLanguage; this.comment = props.comment; this.inputPath = props.inputPath; this.parameters = props.parameters; this.outputPath = props.outputPath; this.resultPath = props.resultPath; this.resultSelector = props.resultSelector; + this.output = props.output; + this.arguments = props.arguments; this.node.addValidation({ validate: () => this.validateState() }); } @@ -280,7 +351,7 @@ export abstract class State extends Construct implements IChainable { /** * Render the state as JSON */ - public abstract toStateJson(): object; + public abstract toStateJson(stateMachineQueryLanguage?: QueryLanguage): object; /** * Add a retrier to the retry list of this state @@ -405,13 +476,15 @@ export abstract class State extends Construct implements IChainable { } /** - * Render InputPath/Parameters/OutputPath in ASL JSON format + * Render InputPath/Parameters/OutputPath/Arguments/Output in ASL JSON format */ protected renderInputOutput(): any { return { InputPath: renderJsonPath(this.inputPath), Parameters: this.parameters, OutputPath: renderJsonPath(this.outputPath), + Arguments: this.arguments, + Output: this.output, }; } @@ -487,6 +560,21 @@ export abstract class State extends Construct implements IChainable { }; } + /** + * Render QueryLanguage in ASL JSON format if needed. + */ + protected renderQueryLanguage(stateMachineQueryLanguage?: QueryLanguage): any { + stateMachineQueryLanguage = stateMachineQueryLanguage ?? QueryLanguage.JSONPATH; + if (stateMachineQueryLanguage === QueryLanguage.JSONATA && this.queryLanguage === QueryLanguage.JSONPATH) { + throw new Error(`'queryLanguage' can not be 'JSONPath' if set to 'JSONata' for whole state machine ${this.node.path}`); + } + const queryLanguage = stateMachineQueryLanguage === QueryLanguage.JSONPATH && this.queryLanguage === QueryLanguage.JSONATA + ? QueryLanguage.JSONATA : undefined; + return { + QueryLanguage: queryLanguage, + }; + } + /** * Called whenever this state is bound to a graph * @@ -608,6 +696,7 @@ function renderCatch(c: CatchTransition) { return { ErrorEquals: c.props.errors, ResultPath: renderJsonPath(c.props.resultPath), + Output: c.props.output, Next: c.next.stateId, }; } diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/succeed.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/succeed.ts index 80eab204832c4..71f648701cef9 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/succeed.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/succeed.ts @@ -1,47 +1,23 @@ import { Construct } from 'constructs'; import { StateType } from './private/state-type'; -import { State } from './state'; -import { INextable } from '../types'; +import { JsonataCommonOptions, JsonPathCommonOptions, State, StateBaseProps } from './state'; +import { INextable, QueryLanguage } from '../types'; +interface SucceedBaseProps extends StateBaseProps {} +interface SucceedJsonPathOptions extends JsonPathCommonOptions {} +interface SucceedJsonataOptions extends JsonataCommonOptions {} +/** + * Properties for defining a Succeed state that using JSONPath + */ +export interface SucceedJsonPathProps extends SucceedBaseProps, SucceedJsonPathOptions {} +/** + * Properties for defining a Succeed state that using JSONata + */ +export interface SucceedJsonataProps extends SucceedBaseProps, SucceedJsonataOptions {} /** * Properties for defining a Succeed state */ -export interface SucceedProps { - /** - * Optional name for this state - * - * @default - The construct ID will be used as state name - */ - readonly stateName?: string; - - /** - * An optional description for this state - * - * @default No comment - */ - readonly comment?: string; - - /** - * JSONPath expression to select part of the state to be the input to this state. - * - * May also be the special value JsonPath.DISCARD, which will cause the effective - * input to be the empty object {}. - * - * @default $ - */ - readonly inputPath?: string; - - /** - * JSONPath expression to select part of the state to be the output to this state. - * - * May also be the special value JsonPath.DISCARD, which will cause the effective - * output to be the empty object {}. - * - * @default $ - */ - readonly outputPath?: string; - -} +export interface SucceedProps extends SucceedBaseProps, SucceedJsonPathOptions, SucceedJsonataOptions {} /** * Define a Succeed state in the state machine @@ -49,6 +25,25 @@ export interface SucceedProps { * Reaching a Succeed state terminates the state execution in success. */ export class Succeed extends State { + /** + * Define a Succeed state in the state machine + * + * Reaching a Succeed state terminates the state execution in success. + */ + public static jsonPath(scope: Construct, id: string, props: SucceedJsonPathProps = {}) { + return new Succeed(scope, id, props); + } + /** + * Define a Succeed state in the state machine + * + * Reaching a Succeed state terminates the state execution in success. + */ + public static jsonata(scope: Construct, id: string, props: SucceedJsonataProps = {}) { + return new Succeed(scope, id, { + ...props, + queryLanguage: QueryLanguage.JSONATA, + }); + } public readonly endStates: INextable[] = []; constructor(scope: Construct, id: string, props: SucceedProps = {}) { @@ -58,9 +53,10 @@ export class Succeed extends State { /** * Return the Amazon States Language object for this state */ - public toStateJson(): object { + public toStateJson(queryLanguage?: QueryLanguage): object { return { Type: StateType.SUCCEED, + ...this.renderQueryLanguage(queryLanguage), Comment: this.comment, ...this.renderInputOutput(), }; diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/task-base.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/task-base.ts index ad3d364dabd2c..4dc22f56397fd 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/task-base.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/task-base.ts @@ -1,5 +1,5 @@ import { Construct } from 'constructs'; -import { renderJsonPath, State } from './state'; +import { JsonataCommonOptions, JsonPathCommonOptions, renderJsonPath, State, StateBaseProps } from './state'; import * as cloudwatch from '../../../aws-cloudwatch'; import * as iam from '../../../aws-iam'; import * as cdk from '../../../core'; @@ -7,72 +7,9 @@ import { Chain } from '../chain'; import { FieldUtils } from '../fields'; import { StateGraph } from '../state-graph'; import { Credentials } from '../task-credentials'; -import { CatchProps, IChainable, INextable, RetryProps } from '../types'; - -/** - * Props that are common to all tasks - */ -export interface TaskStateBaseProps { - /** - * Optional name for this state - * - * @default - The construct ID will be used as state name - */ - readonly stateName?: string; - - /** - * An optional description for this state - * - * @default - No comment - */ - readonly comment?: string; - - /** - * JSONPath expression to select part of the state to be the input to this state. - * - * May also be the special value JsonPath.DISCARD, which will cause the effective - * input to be the empty object {}. - * - * @default - The entire task input (JSON path '$') - */ - readonly inputPath?: string; - - /** - * JSONPath expression to select select a portion of the state output to pass - * to the next state. - * - * May also be the special value JsonPath.DISCARD, which will cause the effective - * output to be the empty object {}. - * - * @default - The entire JSON node determined by the state input, the task result, - * and resultPath is passed to the next state (JSON path '$') - */ - readonly outputPath?: string; - - /** - * JSONPath expression to indicate where to inject the state's output - * - * May also be the special value JsonPath.DISCARD, which will cause the state's - * input to become its output. - * - * @default - Replaces the entire input with the result (JSON path '$') - */ - readonly resultPath?: string; - - /** - * The JSON that will replace the state's raw result and become the effective - * result before ResultPath is applied. - * - * You can use ResultSelector to create a payload with values that are static - * or selected from the state's raw result. - * - * @see - * https://docs.aws.amazon.com/step-functions/latest/dg/input-output-inputpath-params.html#input-output-resultselector - * - * @default - None - */ - readonly resultSelector?: { [key: string]: any }; +import { CatchProps, IChainable, INextable, QueryLanguage, RetryProps } from '../types'; +interface TaskStateBaseOptions extends StateBaseProps { /** * Timeout for the task * @@ -135,6 +72,42 @@ export interface TaskStateBaseProps { readonly credentials?: Credentials; } +interface TaskStateBaseJsonPathOptions extends JsonPathCommonOptions { + /** + * JSONPath expression to indicate where to inject the state's output + * + * May also be the special value JsonPath.DISCARD, which will cause the state's + * input to become its output. + * + * @default $ + */ + readonly resultPath?: string; + + /** + * The JSON that will replace the state's raw result and become the effective + * result before ResultPath is applied. + * + * You can use ResultSelector to create a payload with values that are static + * or selected from the state's raw result. + * + * @see + * https://docs.aws.amazon.com/step-functions/latest/dg/input-output-inputpath-params.html#input-output-resultselector + * + * @default - None + */ + readonly resultSelector?: { [key: string]: any }; +} + +/** + * Props that are common to all tasks that using JSONPath + */ +export interface TaskStateBaseJsonPathProps extends TaskStateBaseOptions, TaskStateBaseJsonPathOptions {} + +/** + * Props that are common to all tasks + */ +export interface TaskStateBaseProps extends TaskStateBaseOptions, TaskStateBaseJsonPathOptions, JsonataCommonOptions {} + /** * Define a Task state in the state machine * @@ -202,8 +175,9 @@ export abstract class TaskStateBase extends State implements INextable { /** * Return the Amazon States Language object for this state */ - public toStateJson(): object { + public toStateJson(queryLanguage?: QueryLanguage): object { return { + ...this.renderQueryLanguage(queryLanguage), ...this.renderNextEnd(), ...this.renderRetryCatch(), ...this.renderTaskBase(), @@ -341,13 +315,15 @@ export abstract class TaskStateBase extends State implements INextable { return { Type: 'Task', Comment: this.comment, - TimeoutSeconds: this.timeout?.toSeconds() ?? this.taskTimeout?.seconds, + TimeoutSeconds: this.timeout?.toSeconds() ?? this.taskTimeout?.seconds ?? this.taskTimeout?.jsonataExpression, TimeoutSecondsPath: this.taskTimeout?.path, - HeartbeatSeconds: this.heartbeat?.toSeconds() ?? this.heartbeatTimeout?.seconds, + HeartbeatSeconds: this.heartbeat?.toSeconds() ?? this.heartbeatTimeout?.seconds ?? this.heartbeatTimeout?.jsonataExpression, HeartbeatSecondsPath: this.heartbeatTimeout?.path, InputPath: renderJsonPath(this.inputPath), OutputPath: renderJsonPath(this.outputPath), ResultPath: renderJsonPath(this.resultPath), + Arguments: this.arguments, + Output: this.output, ...this.renderResultSelector(), ...this.renderCredentials(), }; @@ -423,6 +399,15 @@ export abstract class Timeout { return { seconds: duration.toSeconds() }; } + /** + * Use a dynamic timeout specified by a JSONata expression. + * + * The JSONata expression value must be a positive integer. + */ + public static jsonata(jsonataExpression: string): Timeout { + return { jsonataExpression }; + } + /** * Use a dynamic timeout specified by a path in the state input. * @@ -437,6 +422,11 @@ export abstract class Timeout { */ public abstract readonly seconds?: number; + /** + * JSONata expression for this timeout + */ + public abstract readonly jsonataExpression?: string; + /** * Path for this timeout */ diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/wait.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/wait.ts index d81042f1e2909..6129e466fbcc6 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/states/wait.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/states/wait.ts @@ -1,9 +1,9 @@ import { Construct } from 'constructs'; import { StateType } from './private/state-type'; -import { State } from './state'; +import { JsonataCommonOptions, JsonPathCommonOptions, State, StateBaseProps } from './state'; import * as cdk from '../../../core'; import { Chain } from '../chain'; -import { IChainable, INextable } from '../types'; +import { IChainable, INextable, QueryLanguage } from '../types'; /** * Represents the Wait state which delays a state machine from continuing for a specified time @@ -16,7 +16,18 @@ export class WaitTime { public static duration(duration: cdk.Duration) { return new WaitTime({ Seconds: duration.toSeconds() }); } /** - * Wait until the given ISO8601 timestamp + * Wait for a number of seconds stored in the state object from string. + * This method can use JSONata expression. + * + * If you want to use fixed value, we recommend using `WaitTime.duration()` + * + * Example value: `{% $waitSeconds %}` + */ + public static seconds(seconds: string) { return new WaitTime({ Seconds: seconds }); } + + /** + * Wait until the given ISO8601 timestamp. + * This method can use JSONata expression. * * Example value: `2016-03-14T01:59:00Z` */ @@ -46,29 +57,27 @@ export class WaitTime { } } -/** - * Properties for defining a Wait state - */ -export interface WaitProps { - /** - * Optional name for this state - * - * @default - The construct ID will be used as state name - */ - readonly stateName?: string; - - /** - * An optional description for this state - * - * @default No comment - */ - readonly comment?: string; - +interface WaitBaseProps extends StateBaseProps { /** * Wait duration. */ readonly time: WaitTime; } +interface WaitJsonPathOptions extends JsonPathCommonOptions {} +interface WaitJsonataOptions extends JsonataCommonOptions {} + +/** + * Properties for defining a Wait state that using JSONPath + */ +export interface WaitJsonPathProps extends WaitBaseProps, WaitJsonPathOptions {} +/** + * Properties for defining a Wait state that using JSONata + */ +export interface WaitJsonataProps extends WaitBaseProps, WaitJsonataOptions {} +/** + * Properties for defining a Wait state + */ +export interface WaitProps extends WaitBaseProps {} /** * Define a Wait state in the state machine @@ -76,6 +85,25 @@ export interface WaitProps { * A Wait state can be used to delay execution of the state machine for a while. */ export class Wait extends State implements INextable { + /** + * Define a Wait state using JSONPath in the state machine + * + * A Wait state can be used to delay execution of the state machine for a while. + */ + public static jsonPath(scope: Construct, id: string, props: WaitJsonPathProps) { + return new Wait(scope, id, props); + } + /** + * Define a Wait state using JSONata in the state machine + * + * A Wait state can be used to delay execution of the state machine for a while. + */ + public static jsonata(scope: Construct, id: string, props: WaitJsonataProps) { + return new Wait(scope, id, { + ...props, + queryLanguage: QueryLanguage.JSONATA, + }); + } public readonly endStates: INextable[]; private readonly time: WaitTime; @@ -98,9 +126,10 @@ export class Wait extends State implements INextable { /** * Return the Amazon States Language object for this state */ - public toStateJson(): object { + public toStateJson(queryLanguage?: QueryLanguage): object { return { Type: StateType.WAIT, + ...this.renderQueryLanguage(queryLanguage), Comment: this.comment, ...this.time._json, ...this.renderNextEnd(), diff --git a/packages/aws-cdk-lib/aws-stepfunctions/lib/types.ts b/packages/aws-cdk-lib/aws-stepfunctions/lib/types.ts index 688af6e3875eb..5e72155111962 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/lib/types.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/lib/types.ts @@ -181,6 +181,20 @@ export interface CatchProps { * @default $ */ readonly resultPath?: string; + + /** + * Used to specify and transform output from the state. + * When specified, the value overrides the state output default. + * The output field accepts any JSON value (object, array, string, number, boolean, null). + * Any string value, including those inside objects or arrays, + * will be evaluated as JSONata if surrounded by {% %} characters. + * Output also accepts a JSONata expression directly. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/concepts-input-output-filtering.html + * + * @default - None + */ + readonly output?: { [key: string]: any }; } /** @@ -242,3 +256,21 @@ export interface ProcessorConfig { * @deprecated use JsonPath.DISCARD */ export const DISCARD = 'DISCARD'; + +/** + * The name of the query language used by the state machine or state. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/transforming-data.html + * + * @default JSONPATH + */ +export enum QueryLanguage { + /** + * Use JSONPath + */ + JSONPATH = 'JSONPath', + /** + * Use JSONata + */ + JSONATA = 'JSONata', +} diff --git a/packages/aws-cdk-lib/aws-stepfunctions/test/custom-state.test.ts b/packages/aws-cdk-lib/aws-stepfunctions/test/custom-state.test.ts index f332c25924f71..c0e305a3d06fc 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/test/custom-state.test.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/test/custom-state.test.ts @@ -36,6 +36,7 @@ describe('Custom State', () => { expect(customState.toStateJson()).toStrictEqual({ ...stateJson, ...{ Catch: undefined, Retry: undefined }, + QueryLanguage: undefined, End: true, }); }); diff --git a/packages/aws-cdk-lib/aws-stepfunctions/test/distributed-map.test.ts b/packages/aws-cdk-lib/aws-stepfunctions/test/distributed-map.test.ts index 34e4428ae276f..99abd4fbf108f 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/test/distributed-map.test.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/test/distributed-map.test.ts @@ -754,6 +754,53 @@ describe('Distributed Map State', () => { }); }), + test('State Machine With Distributed Map State with JSONata', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const map = stepfunctions.DistributedMap.jsonata(stack, 'Map State', { + maxConcurrency: 1, + items: stepfunctions.ProvideItems.jsonata('{% $inputForMap %}'), + itemSelector: { + foo: 'foo', + bar: '{% $bar %}', + }, + }); + map.itemProcessor(new stepfunctions.Pass(stack, 'Pass State')); + + // THEN + expect(render(map)).toStrictEqual({ + StartAt: 'Map State', + States: { + 'Map State': { + Type: 'Map', + QueryLanguage: 'JSONata', + End: true, + Items: '{% $inputForMap %}', + ItemSelector: { + foo: 'foo', + bar: '{% $bar %}', + }, + ItemProcessor: { + ProcessorConfig: { + Mode: stepfunctions.ProcessorMode.DISTRIBUTED, + ExecutionType: stepfunctions.StateMachineType.STANDARD, + }, + StartAt: 'Pass State', + States: { + 'Pass State': { + Type: 'Pass', + End: true, + }, + }, + }, + MaxConcurrency: 1, + }, + }, + }); + }), + test('synth is successful', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.DistributedMap(stack, 'Map State', { diff --git a/packages/aws-cdk-lib/aws-stepfunctions/test/fail.test.ts b/packages/aws-cdk-lib/aws-stepfunctions/test/fail.test.ts index 0bdac483cede6..1f4223e820cd4 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/test/fail.test.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/test/fail.test.ts @@ -64,4 +64,27 @@ describe('Fail State', () => { }, ); }); + test('Fail State using JSONata', () => { + // WHEN + const definition = stepfunctions.Fail.jsonata(stack, 'JSONataFail', { + queryLanguage: stepfunctions.QueryLanguage.JSONATA, + cause: '{% $cause %}', + error: '{% $error %}', + }); + + // THEN + expect(render(stack, definition)).toStrictEqual( + { + StartAt: 'JSONataFail', + States: { + JSONataFail: { + Type: 'Fail', + QueryLanguage: 'JSONata', + Cause: '{% $cause %}', + Error: '{% $error %}', + }, + }, + }, + ); + }); }); diff --git a/packages/aws-cdk-lib/aws-stepfunctions/test/map.test.ts b/packages/aws-cdk-lib/aws-stepfunctions/test/map.test.ts index be09ab9282609..2414e1488cada 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/test/map.test.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/test/map.test.ts @@ -272,6 +272,53 @@ describe('Map State', () => { }); }), + test('State Machine With Map State using JSONata', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const map = stepfunctions.Map.jsonata(stack, 'Map State', { + stateName: 'My-Map-State', + maxConcurrency: 1, + items: stepfunctions.ProvideItems.jsonata('{% $inputForMap %}'), + itemSelector: { + foo: 'foo', + bar: '{% $bar %}', + }, + }); + map.itemProcessor(new stepfunctions.Pass(stack, 'Pass State')); + + // THEN + expect(render(map)).toStrictEqual({ + StartAt: 'My-Map-State', + States: { + 'My-Map-State': { + Type: 'Map', + QueryLanguage: 'JSONata', + End: true, + ItemSelector: { + foo: 'foo', + bar: '{% $bar %}', + }, + ItemProcessor: { + ProcessorConfig: { + Mode: 'INLINE', + }, + StartAt: 'Pass State', + States: { + 'Pass State': { + Type: 'Pass', + End: true, + }, + }, + }, + Items: '{% $inputForMap %}', + MaxConcurrency: 1, + }, + }, + }); + }), + test('synth is successful with iterator', () => { const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { @@ -490,6 +537,32 @@ describe('Map State', () => { test('isPositiveInteger is true with max integer value', () => { expect(stepfunctions.isPositiveInteger(Number.MAX_SAFE_INTEGER)).toEqual(true); + }), + + test('items from JSON array', () => { + // GIVEN + const jsonArray = [1, '{% $two %}', 3]; + + // WHEN + const items = stepfunctions.ProvideItems.jsonArray(jsonArray); + + // THEN + expect(items).toEqual({ + items: [1, '{% $two %}', 3], + }); + }), + + test('items from JSONata expression', () => { + // GIVEN + const jsonataExpression = '{% $items %}'; + + // WHEN + const items = stepfunctions.ProvideItems.jsonata(jsonataExpression); + + // THEN + expect(items).toEqual({ + items: '{% $items %}', + }); }); }); diff --git a/packages/aws-cdk-lib/aws-stepfunctions/test/parallel.test.ts b/packages/aws-cdk-lib/aws-stepfunctions/test/parallel.test.ts index 2b8eb8b80dda6..3761e1dfda327 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/test/parallel.test.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/test/parallel.test.ts @@ -58,6 +58,39 @@ describe('Parallel State', () => { }, }); }); + + test('State Machine With Parallel State using JSONata', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const parallel = stepfunctions.Parallel.jsonata(stack, 'Parallel State', { + output: { + foo: '{% $state.input.result[0] %}', + }, + }); + parallel.branch(stepfunctions.Pass.jsonPath(stack, 'Branch 1', { stateName: 'first-pass-state' })); + parallel.branch(stepfunctions.Pass.jsonPath(stack, 'Branch 2')); + + // THEN + expect(render(parallel)).toStrictEqual({ + StartAt: 'Parallel State', + States: { + 'Parallel State': { + Type: 'Parallel', + QueryLanguage: 'JSONata', + End: true, + Branches: [ + { StartAt: 'first-pass-state', States: { 'first-pass-state': { Type: 'Pass', End: true } } }, + { StartAt: 'Branch 2', States: { 'Branch 2': { Type: 'Pass', End: true } } }, + ], + Output: { + foo: '{% $state.input.result[0] %}', + }, + }, + }, + }); + }); }); function render(sm: stepfunctions.IChainable) { diff --git a/packages/aws-cdk-lib/aws-stepfunctions/test/private/fake-task.ts b/packages/aws-cdk-lib/aws-stepfunctions/test/private/fake-task.ts index fe5c69b880230..6c4a0059825ba 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/test/private/fake-task.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/test/private/fake-task.ts @@ -4,6 +4,7 @@ import * as sfn from '../../lib'; export interface FakeTaskProps extends sfn.TaskStateBaseProps { readonly metrics?: sfn.TaskMetricsConfig; + readonly queryLanguage?: sfn.QueryLanguage; } export class FakeTask extends sfn.TaskStateBase { @@ -19,11 +20,18 @@ export class FakeTask extends sfn.TaskStateBase { * @internal */ protected _renderTask(): any { + const param = { + MyParameter: 'myParameter', + }; + if (this.queryLanguage === sfn.QueryLanguage.JSONATA) { + return { + Resource: 'my-resource', + Arguments: param, + }; + } return { Resource: 'my-resource', - Parameters: sfn.FieldUtils.renderObject({ - MyParameter: 'myParameter', - }), + Parameters: sfn.FieldUtils.renderObject(param), }; } -} +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-stepfunctions/test/state-machine-resources.test.ts b/packages/aws-cdk-lib/aws-stepfunctions/test/state-machine-resources.test.ts index 62b088ebe048e..6a124b185699c 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/test/state-machine-resources.test.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/test/state-machine-resources.test.ts @@ -222,6 +222,8 @@ describe('State Machine Resources', () => { ResultPath: undefined, TimeoutSeconds: undefined, HeartbeatSeconds: undefined, + Arguments: undefined, + Output: undefined, }); }), @@ -265,6 +267,8 @@ describe('State Machine Resources', () => { ResultPath: undefined, TimeoutSeconds: undefined, HeartbeatSeconds: undefined, + Arguments: undefined, + Output: undefined, }); }), @@ -720,10 +724,10 @@ describe('State Machine Resources', () => { } }), - test('Pass should render InputPath / Parameters / OutputPath correctly', () => { + test('Pass with JSONPath should render InputPath / Parameters / OutputPath correctly', () => { // GIVEN const stack = new cdk.Stack(); - const task = new stepfunctions.Pass(stack, 'Pass', { + const task = stepfunctions.Pass.jsonPath(stack, 'Pass', { stateName: 'my-pass-state', inputPath: '$', outputPath: '$.state', @@ -753,9 +757,51 @@ describe('State Machine Resources', () => { 'arrayArgument': ['a', 'b', 'c'], }, Type: 'Pass', + QueryLanguage: undefined, + Comment: undefined, + Result: undefined, + ResultPath: undefined, + Arguments: undefined, + Output: undefined, + }); + }), + + test('Pass with JSONata should render Output correctly', () => { + // GIVEN + const stack = new cdk.Stack(); + const task = stepfunctions.Pass.jsonata(stack, 'Pass', { + stateName: 'my-pass-state', + output: { + input: '{% $states.input %}', + stringArgument: 'inital-task', + numberArgument: 123, + booleanArgument: true, + arrayArgument: ['a', 'b', 'c'], + }, + }); + + // WHEN + const taskState = task.toStateJson(); + + // THEN + expect(taskState).toStrictEqual({ + Type: 'Pass', + QueryLanguage: 'JSONata', + Output: { + input: '{% $states.input %}', + stringArgument: 'inital-task', + numberArgument: 123, + booleanArgument: true, + arrayArgument: ['a', 'b', 'c'], + }, + End: true, Comment: undefined, + InputPath: undefined, Result: undefined, ResultPath: undefined, + Parameters: undefined, + OutputPath: undefined, + Arguments: undefined, }); }), diff --git a/packages/aws-cdk-lib/aws-stepfunctions/test/task-base.test.ts b/packages/aws-cdk-lib/aws-stepfunctions/test/task-base.test.ts index 207c979f21cc9..9282234b2c0bb 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/test/task-base.test.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/test/task-base.test.ts @@ -45,6 +45,35 @@ describe('Task base', () => { }); }); + test('instantiate a concrete implementation with JSONata properties', () => { + // WHEN + task = new FakeTask(stack, 'my-exciting-task', { + queryLanguage: sfn.QueryLanguage.JSONATA, + comment: 'my exciting task', + output: { + FakeName: '{% $states.result.FakeName %}', + }, + }); + + // THEN + expect(renderGraph(task)).toEqual({ + StartAt: 'my-exciting-task', + States: { + 'my-exciting-task': { + End: true, + Type: 'Task', + QueryLanguage: 'JSONata', + Comment: 'my exciting task', + Resource: 'my-resource', + Arguments: { MyParameter: 'myParameter' }, + Output: { + FakeName: '{% $states.result.FakeName %}', + }, + }, + }, + }); + }); + test('instantiate a concrete implementation with credentials of a specified role', () => { // WHEN const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/example-role'); @@ -336,6 +365,24 @@ describe('Task base', () => { })); }); + test('taskTimeout and heartbeatTimeout specified with a JSONata expression', () => { + // WHEN + task = new FakeTask(stack, 'my-exciting-task', { + heartbeatTimeout: sfn.Timeout.jsonata('{% $heartbeat %}'), + taskTimeout: sfn.Timeout.jsonata('{% $timeout %}'), + }); + + // THEN + expect(renderGraph(task)).toEqual(expect.objectContaining({ + States: { + 'my-exciting-task': expect.objectContaining({ + HeartbeatSeconds: '{% $heartbeat %}', + TimeoutSeconds: '{% $timeout %}', + }), + }, + })); + }); + testDeprecated('deprecated props timeout and heartbeat still work', () => { // WHEN task = new FakeTask(stack, 'my-exciting-task', { diff --git a/packages/aws-cdk-lib/aws-stepfunctions/test/wait.test.ts b/packages/aws-cdk-lib/aws-stepfunctions/test/wait.test.ts index 599baeaf44cf3..ec794c6b6f42d 100644 --- a/packages/aws-cdk-lib/aws-stepfunctions/test/wait.test.ts +++ b/packages/aws-cdk-lib/aws-stepfunctions/test/wait.test.ts @@ -18,6 +18,36 @@ describe('Wait State', () => { }); }); + test('wait time from JSONata expression as timestamp', () => { + // GIVEN + const jsonataExpression = '{% $timestamp %}'; + + // WHEN + const waitTime = WaitTime.timestamp(jsonataExpression); + + // THEN + expect(waitTime).toEqual({ + json: { + Timestamp: '{% $timestamp %}', + }, + }); + }); + + test('wait time from JSONata expression as seconds', () => { + // GIVEN + const jsonataExpression = '{% $seconds %}'; + + // WHEN + const waitTime = WaitTime.seconds(jsonataExpression); + + // THEN + expect(waitTime).toEqual({ + json: { + Seconds: '{% $seconds %}', + }, + }); + }); + test('wait time from seconds path in state object', () => { // GIVEN const secondsPath = '$.waitSeconds';