Redashの動的クエリをJavascriptから呼び出す

社内ではRedashを活用しています。Redashには固定のSQLを実行してビジュアライズする以外にも、一部の値をパラメータ化した動的なクエリを実行する機能もあります。
この動的クエリを外部からAPIとして、ChromeExtensionから使おうとしたらドハマリしたのでまとめておきます。


Redashの動的クエリ
---------------------

以下のようにSQL内に`{{hoge}}`と書いたSQLを指定することで利用できます。

```sql
SELECT account_name
FROM member
WHERE login_id = '{{login_id}}'
```

このようなパラメータ付のSQLを指定すると以下の様な入力欄が自動で生成されます。

![New_Query.png](https://qiita-image-store.s3.amazonaws.com/0/44722/67d1e040-7eae-d99b-a1ab-bfdccd08a39a.png)


実は入力欄に値を設定する以外にもURLのクエリパラメータとして指定することができます。

`http://your.redash.host/queries/<query_id>/source?p_login_id=100`

指定する値は`p_parameter=value`です。`login_id`というパラメータに100を指定するのであれば`p_login_id=100`をURLに加えればよいわけです。

これを外部から叩くことができれば自前のサーバを用意せずにAPIとして利用できる....
と思ったら一筋縄では行きませんでした。

外部からの叩き方
--------------------

パラメータを使わないクエリであれば、クエリ編集画面(source)の以下から出てくるURLを叩くだけでJSONなりCSVなりで結果が取得できます。

![_Mail__return_ratio_per_comapny_and_woker_type.png](https://qiita-image-store.s3.amazonaws.com/0/44722/b90784b0-c612-a22c-07cd-40df33a27361.png)

しかし、パラメータ付クエリの場合同じ方法を用いると期待した結果を得られません。少なくともパラメータを指定することができず、最後に成功したクエリの結果が戻る動作になっているようです。

[[Feature Request] Add support for query parameters in the API](https://github.com/getredash/redash/issues/285)

というissueが存在し、Closeになっていたのでそれ用のAPIがあるのかなと思ったら

> Actually this is already possible with the refresh API. See this for example:
> https://gist.github.com/arikfr/e3e434d8cfd7f331d499ccf351abbff9

と閉められていました。どうやら`refresh`APIを用いて実現できるからそれをしろとのことでした。意外とこれが面倒です。

ざっくりと流れ的には以下でした。

* 指定したIDのクエリにパラメータを渡して結果を更新するようリクエスト(refresh)
* リクエストに対してJOBのIDが戻るためJOBのステータスをポーリング
* ジョブが完了すると`query_result_id`が戻るためそれを用いて結果を取得

ただパラメータ付の結果がほしいだけなのに3回以上リクエストしないといけないのです。

issueで示されている[Pythonでのsample](https://gist.github.com/arikfr/e3e434d8cfd7f331d499ccf351abbff9)や[Redash APIで動的クエリ発行するモジュール](https://qiita.com/toritori0318/items/25b0998fe38c023451f1)の流れを参考に、Javascriptで実行してみました。

### USER Tokenの発行

この手順には各クエリの`show API Key`で生成されるキーではなく、ユーザ単位で発行されるAPI Keyが必要です。
`Settings > Users > ユーザ名 > API KEY`からAPI Keyを確認しておきます。



### Javascriptでの実装

Ajaxだけできればいいので、jQuery等は利用せず`fetch`を使います。もちろん`$.ajax`でも`axios`でも同じ様な流れになると思います。3回以上Ajaxしなければいけないので、普通にやるとコールバックだらけで辛いのでasync await で同期的に書いています。

以下はクエリID:99に対して`login_id`というパラメータを渡して実行しています。

```javascript
async function accessRedash(accountId){
    const API_TOKEN = 'YOUR USER TOKEN not query token.';
    // クエリパラメータ付でrefresh apiを実行
    let response = await fetch('http://your.redash.host/api/queries/99/refresh?p_login_id=' + accountId, {
        method: 'POST',
        headers: {
            'Authorization': `Key ${API_TOKEN}`,
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        }
    }).then(res => {
        return res.json();
    })
    // refresh apiの戻り値にはjobが入っている
    let job = response['job'];
    // job のステータスが3,4になるまで実行(refreshが完了すると脱出する)
    while (job.status !== 3 && job.status !== 4) {
        // job apiで最新のジョブの状況をチェック
        response = await fetch('http://your.redash.host/api/jobs/' + job['id'], {
            headers: {
                'Authorization': `Key ${API_TOKEN}`,
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }
        }).then(res => {
            return res.json();
        })
        job = response['job'];
        // 0.5秒とりあえず待つ
        await sleep(500);
    }
    // jobが完了している場合query_result_idが手に入る
    const resultId = job['query_result_id'];
    // query_result_id で指定される結果をJSONで受け取る(.jsonと指定することでJSON形式になる)
    response = await fetch(`http://your.redash.host/api/queries/99/results/${resultId}.json`,{
        headers: {
            'Authorization': `Key ${API_TOKEN}`,
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        }
    }).then(res => {
        return res.json();
    })

    // query_result.dataに配列で結果が格納されているので後は好きにする
    const resultSet = response.query_result.data;
    console.log(resultSet)

}

// sleep helper
function sleep(msec){
    return new Promise(resolve => setTimeout(resolve, msec));
}
```

async awaitで楽にかけますが、素直に書いてたら大変ネストが深くなって辛かっただろうなと思います。Redash 3.0で試しましたがとにかくこのあたりのドキュメントが少なくて苦労しましたのでQiitaに記事として残させていただきます。