😑

step functionsで配列にオブジェクトを追加したいだけなのに

2024/10/24に公開

記事の内容

コード書いていると配列へのオブジェクト要素の追加とかよくあると思います。
step functionsで同様のことをしようとしたとき、一筋縄でいかなかったのでこちらに記載しておこうと思います。

関係する技術・ツール

  • AWS Step Functions

したいこと

  • 文章にするより、コード例を提示したほうが一目瞭然だと思います。下記のようなことがしたいのです。
example.js
const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Charlie" },
];

//追加
users.push({ id: 4, name: "David" });

//これでもいい
const newUsers = [...users, { id: 5, name: "Eve" }];

組み込み関数を見てみる

  • step functionsには組み込み関数あるのでできそうと思っていました。しかし、pushとか、appendとか、mergeとかそれっぽいものはありません・・・

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/intrinsic-functions.html#asl-intrsc-func-arrays

どうしたのか

  • 組み込み関数を複数駆使して実装しました。下記はCognitoのUpdateIdentityPoolを実行する例です。
sample.json
    "AppendIdentityProviderItem": {
      "Type": "Pass",
      "ResultPath": "$.AppendIdentityProviderItem",
      "Parameters": {
        "ProviderName": "cognito-idp.region.amazonaws.com/ユーザープールID",
        "ClientId.$": "$.CreateUserPoolClientResult.ClientId",
        "ServerSideTokenCheck": false
      },
      "Next": "UpdateIdentityPool"
    },
    "UpdateIdentityPool": {
      "Next": "PutSamlConfigMetadata",
      "Type": "Task",
      "Parameters": {
        "IdentityPoolId": "アイデンティティプールID",
        "IdentityPoolName": "アイデンティティプール名",
        "AllowUnauthenticatedIdentities": true,
        "CognitoIdentityProviders.$": "States.StringToJson(States.Format('[{},{}]', States.ArrayGetItem(States.StringSplit(States.JsonToString($.DescribeIdentityPoolResult.CognitoIdentityProviders), '[]'),0), States.JsonToString($.AppendIdentityProviderItem)))"
      },
      "Resource": "arn:aws:states:::aws-sdk:cognitoidentity:updateIdentityPool",
      "ResultPath": null
    },

CognitoIdentityProvidersは配列オブジェクトなのですが、ここに要素を追加する必要がありました。しかし、このAPIの仕様的にCognitoIdentityProvidersは置換なので純粋にアイテムの追加をしたい場合、既存のCognitoIdentityProvidersのアイテムは含めつつ、新規アイテムを追加する必要がありました。
そのため、既存のアイテムの配列に対して新規アイテム追加ないし、既存のアイテムの配列と新規のアイテムの配列をマージするようなことが求められます。

上記のCognitoIdentityProviders.$にワンライナーで組み込み関数を駆使している箇所が今回の悩まされた箇所です。。。

https://docs.aws.amazon.com/ja_jp/cognitoidentity/latest/APIReference/API_UpdateIdentityPool.html

何をやっているのか

  • 関数のネストが多すぎて読む気にならないと思うので、インデントを付けて確認してみたいと思います。
States.StringToJson( ⑤
    States.Format( ④
        '[{},{}]',
        States.ArrayGetItem( ③
            States.StringSplit( ②
                States.JsonToString( ①
                    $.DescribeIdentityPoolResult.CognitoIdentityProviders
                ),
                '[]'
            ),
            0
        ),
        States.JsonToString(
            $.AppendIdentityProviderItem
        )
    )
)


$.DescribeIdentityPoolResult.CognitoIdentityProvidersは既存のCognitoIdentityProvidersです。DescribeIdentityPool APIをコールして前State取得している想定です。下記のような配列です。これをエスケープされたJsonへ変換しています。

[
    {
        "ProviderName": "ユーザープールID1",
        "ClientId.$": "クライアントID1",
        "ServerSideTokenCheck": false
    },
    {
        "ProviderName": "ユーザープールID2",
        "ClientId.$": "クライアントID2",
        "ServerSideTokenCheck": false
    },
]

①の結果を[]で文字列として分割しています。つまり下記のような結果になります。


[
    "\"{\"ProviderName\":\"ユーザープールID1\",\"ClientId.$\":\"クライアントID1\",\"ServerSideTokenCheck\":false},{\"ProviderName\":\"ユーザープールID2\",\"ClientId.$\":\"クライアントID2\",\"ServerSideTokenCheck\":false}\""
]

ポイントは①の配列の中身全てが、文字列として新たに配列の0番目になったことです。要素は""なので文字列になっていることがわかります。

②の結果の0要素目を取得しています。つまり下記を取得しています。

    "\"{\"ProviderName\":\"ユーザープールID1\",\"ClientId.$\":\"クライアントID1\",\"ServerSideTokenCheck\":false},{\"ProviderName\":\"ユーザープールID2\",\"ClientId.$\":\"クライアントID2\",\"ServerSideTokenCheck\":false}\""

③の結果と$.AppendIdentityProviderItem(新たに追加するアイテム)をState.Formatでバインドしています。State.Formatは{}をプレイスホルダとして、与えられた引数を文字列としてバインドする関数です。
つまり、下記のようなことが行われています。

"[{\"ProviderName\":\"ユーザープールID1\",\"ClientId.$\":\"クライアントID1\",\"ServerSideTokenCheck\":false},{\"ProviderName\":\"ユーザープールID2\",\"ClientId.$\":\"クライアントID2\",\"ServerSideTokenCheck\":false}, {\"ProviderName\":\"新規追加ユーザープールID1\",\"ClientId.$\":\"新規追加クライアントID1\",\"ServerSideTokenCheck\":false}]"

④の結果の文字列をエスケープされていないJsonへ変換しています。④の時点で見慣れたエスケープされたJsonになっているので問題なく変換できます。結果も既存のアイテムと新規のアイテムともにあります。これで期待通りの処理ができるというわけです。

まとめ

かなり周りくどい感じですが、配列へのオブジェクトの追加、マージが一応できました。
しかし正直、複雑な配列処理の場合はLambdaを使って、慣れた言語で配列の処理したほうが可読性的にはよいと思います。
配列処理のためだけにLambda使わないといけないの?て感じですが、step functionsでこのあたりの組み込み関数の実装に今後期待したいです。
他によい方法知っている方いましたら、ぜひ教えてください。

NCDCエンジニアブログ

Discussion