Node-REDで作成したページから、CSVをダウンロードする方法

##背景
Node-REDで作ったページから、cloudantに格納した情報をCSV形式でダウンロードしたいと思いました。
ググるとfileノードを使う方法はすぐに見つかるのですが、IBM CloudのNode-REDにはfileノードがありませんでした。
ローカルPCにインストールしたNode-REDにはfileノードがありましたが、今回はIBM Cloudからダウンロードしたかったので、fileノードを使わない方法を探す必要がありました。
なんとかうまくいったので、備忘録として残しておきます。

##参考文献
- [Node-REDで温度などの時系列データをCSVファイルに保存する](https://tibbo-pi.co-works.co.jp/blog/node-red%E3%81%A7%E6%B8%A9%E5%BA%A6%E3%81%AA%E3%81%A9%E3%81%AE%E6%99%82%E7%B3%BB%E5%88%97%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92csv%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AB%E4%BF%9D%E5%AD%98%E3%81%99/)
- [CSVを生成して出力](https://cookbook.nodered.jp/basic/generate-csv)
- [Node-RED で（普通の）ウェブアプリケーションを作る](http://dotnsf.blog.jp/archives/1068493303.html)

##手順
###１．アプリケーションを作る
[Node-RED で（普通の）ウェブアプリケーションを作る](http://dotnsf.blog.jp/archives/1068493303.html) を参考にアプリケーションを作ります。
cloudantの名前は**downloadtest**とします。

###２．ダウンロードのリンクを作る
templateの59，60行目に次の２行を追記します。

```
<br/>
<a id="downloadtests" href="/downloadtests" download="test.csv" onclick="handleDownload()">ダウンロード</a>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/d9477f24-ee9d-b030-5ea7-6376be00033f.png)

###３．ダウンロードのリンク先を作る
まず、http in、cloudant in、split、function、join、converter、http responseをフロー上に配置して一直線につなぎます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/e775768e-822e-076b-b6db-e73fd3d9c27e.png)

続いて、一つずつ設定していきます。

http in のプロパティです。
メソッドはGETを選び、URLに/downloadtestsを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/6108a1b9-cde3-75fa-0c0b-c35feb3eb1a6.png)

cloudant in のプロパティです。
Databaseにdownloadtestを指定し、Search byはall documentsを選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/3b34f544-03bf-654f-dd84-6ab36865aef9.png)

splitのプロパティです。
デフォルトのまま使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/9d24b5d7-186c-c3fc-1523-6606c6115668.png)

functionのプロパティです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/b6607378-be3f-08d6-3deb-55d13c6400e9.png)


```
var country = msg.payload.country;
var capital = msg.payload.capital;
var csv = {
    type: 'text',
    text: country + ',' + capital };
msg.payload = csv.text;
return msg;
```

joinのプロパティです。
動作を手動に変更します。
変更したら、あとはデフォルトのまま使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/6a49cbee-bb65-4d75-1a99-17271ea83ba7.png)


converterのプロパティです。
Windowsで扱えるようにEncodingにはShift_JISを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/127c41c2-b87c-1a6e-9dce-a4fe14306712.png)

最後にhttp responseのプロパティです。
デフォルトのまま使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/38bd8579-54fd-d23b-e127-294aa4642db0.png)

###４．デプロイする
デプロイをクリックします。

###５．downloadtestにアクセスする
ブラウザを開いて、https://<アプリケーション>.mybluemix.net/downloadtest にアクセスします。
次の画面が表示されるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/840890fc-a942-c966-2f30-5d5603d4d26a.png)

###６．データを入れる
CountryとCapitalに情報を入れます。
動作検証のため、アルファベット、漢字、カタカナを混ぜます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/7cd2abde-55d0-a14c-c2f4-28f1748acd98.png)

###７．ダウンロードする
ダウンロードのリンクをクリックすると、次のポップアップが出ます。
![無題 (3).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/77edcab2-41ae-0ee5-f1c8-7d0bf718163a.jpeg)

OKを押すとExcelが起動します。
CSVをダウンロードすることができました。
![無題3 (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/37f0dc72-1641-abbb-f991-02d3fc042d54.jpeg)

##並び替えてみる
CSVの中身は入力した順番のままなので、特定の列で並び替えてからダウンロードしてみたいと思います。

splitとfunctionの間にsortを入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/ceb4bc84-3888-b50f-c14c-dac479058046.png)

sortのプロパティです。
国名で並び替えるために、キーにpayload.countryを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/52d3fad2-67f3-1c98-4753-08767730ccfd.png)

デプロイしなおしてダウンロードすると、順番が替わったことがわかります。
![無題2 (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620157/1f79d938-6302-a1bf-2699-988e5653f19b.jpeg)

##ポイント
###１．文字コードを変換する必要がある
converterを使わずにダウンロードすると、日本語がUTF-8形式のため、Excelでは文字化けします。
しかし複数の引数をconverterに渡すと「This node only handles buffers.」というエラーが生じます。
converterはひとつの引数しか扱えないようなので、peyloadから余分な情報を排除する必要がありました。
splitして、functionで情報を抽出してから、joinしなおしているの、文字コードを変換するためです。

###２．functionの中でCSVを作る
functionの5行目にある text: country + ',' + capital で、CSV形式になるようにカンマ区切りの行を作っています。

###３．sortとconverterの位置
色々と位置を変えながら試した結果、今のこの場所にあれば期待通りの結果が得られることがわかりました。

###４．debugはmsgオブジェクト全体を使う
引数はJSON形式で書かれているので、階層と名前を意識する必要があります。
正しい名前を確認するにはdebugが有効なので、debugの対象をmsgオブジェクト全体としてとりあえず実行。
debugの結果から引数名を確認したらコードを修正、デプロイ、実行、debugの結果確認、と繰り返しました。

##まとめ
cloudantの参照先を変えて、functionで抽出したい情報を書き換えれば、どの環境からもCSVをダウンロードできるので便利かと思います。

##コード
今回作成したコードです。

```
[{"id":"e7e549d4.4e7df8","type":"tab","label":"フロー 3","disabled":false,"info":""},{"id":"2c88034e.47371c","type":"http in","z":"e7e549d4.4e7df8","name":"","url":"/myrecord","method":"post","upload":false,"swaggerDoc":"","x":140,"y":80,"wires":[["198e98b1.e899ef","4ccd8561.0be2cc"]]},{"id":"5504c328.474034","type":"cloudant in","z":"e7e549d4.4e7df8","name":"","cloudant":"","database":"downloadtest","service":"mylarning-cloudant-1586217902064-668","search":"_all_","design":"","index":"","x":340,"y":220,"wires":[["ed20c6a3.1a0258"]]},{"id":"4ccd8561.0be2cc","type":"http response","z":"e7e549d4.4e7df8","name":"","statusCode":"","headers":{},"x":300,"y":120,"wires":[]},{"id":"198e98b1.e899ef","type":"cloudant out","z":"e7e549d4.4e7df8","name":"","cloudant":"","database":"downloadtest","service":"mylarning-cloudant-1586217902064-668","payonly":true,"operation":"insert","x":440,"y":80,"wires":[]},{"id":"5ebca37d.1e8d44","type":"http in","z":"e7e549d4.4e7df8","name":"","url":"/myrecords","method":"get","upload":false,"swaggerDoc":"","x":140,"y":220,"wires":[["5504c328.474034"]]},{"id":"ed20c6a3.1a0258","type":"http response","z":"e7e549d4.4e7df8","name":"","statusCode":"","headers":{},"x":530,"y":220,"wires":[]},{"id":"ce0e7fc4.c2d25","type":"http in","z":"e7e549d4.4e7df8","name":"","url":"/downloadtest","method":"get","upload":false,"swaggerDoc":"","x":150,"y":320,"wires":[["33435fb2.aa1018"]]},{"id":"33435fb2.aa1018","type":"template","z":"e7e549d4.4e7df8","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n<head>\n<title>Countries & Capitals</title>\n<script src='//code.jquery.com/jquery-2.2.4.min.js'></script>\n<script>\n$(function(){\n  $('#myform').submit( function(){\n    $.ajax({\n      type: 'POST',\n      url: '/myrecord',\n      data: { country: $('#country').val(), capital: $('#capital').val() },\n      success: function( data ){\n        console.log( data );\n        setTimeout( 'list()', 1000 );\n      },\n      error: function(){\n        console.log( 'error' );\n      }\n    });\n    return false;\n  });\n  list();\n});\nfunction list(){\n  $('#list').html( '' );\n  $.ajax({\n    type: 'GET',\n    url: '/myrecords',\n    success: function( data ){\n      if( data && data.length > 0 ){\n        for( var i = 0; i < data.length; i ++ ){\n          var doc = data[i];\n          var tr = '<tr><td>' + doc.country + '</td><td>' + doc.capital + '</td></tr>';\n          $('#list').append( tr );\n        }\n      }\n    },\n    error: function(){\n      console.log( 'error' );\n    }\n  });\n}\n</script>\n</head>\n<body>\n  <table border='1'>\n  <thead>\n  <tr><th>Country</th><th>Capital</th><tr>\n  </thead>\n  <tbody id='list'>\n  </tbody>\n</table>\n<hr/>\n\n<form method='POST' id='myform'>\n  Country: <input type='text' id='country' name='country' value=''/><br/>\n  Capital: <input type='text' id='capital' name=\"capital\" value=''/><br/>\n  <input type='submit' value='click'/>\n  <br/>\n  <a id=\"downloadtests\" href=\"/downloadtests\" download=\"test.csv\" onclick=\"handleDownload()\">ダウンロード</a>\n</form>\n</body>\n</html>","output":"str","x":340,"y":320,"wires":[["854fd3ad.ee2458"]]},{"id":"854fd3ad.ee2458","type":"http response","z":"e7e549d4.4e7df8","name":"","statusCode":"","headers":{},"x":510,"y":320,"wires":[]},{"id":"6a2edd8e.fcf254","type":"http in","z":"e7e549d4.4e7df8","name":"","url":"/downloadtests","method":"get","upload":false,"swaggerDoc":"","x":160,"y":440,"wires":[["39e140f5.e52d88"]]},{"id":"ce102314.8d30f8","type":"http response","z":"e7e549d4.4e7df8","name":"","statusCode":"","headers":{},"x":1190,"y":560,"wires":[]},{"id":"39e140f5.e52d88","type":"cloudant in","z":"e7e549d4.4e7df8","name":"","cloudant":"","database":"downloadtest","service":"mylarning-cloudant-1586217902064-668","search":"_all_","design":"searchAll","index":"searchAll","x":400,"y":440,"wires":[["1296359d.5eabea","ae77dcec.fa83e8"]]},{"id":"40b5706f.2cef1","type":"function","z":"e7e549d4.4e7df8","name":"必要な情報を抽出。一行ごとにまとめる","func":"var country = msg.payload.country;\nvar capital = msg.payload.capital;\nvar csv = {\n    type: 'text',\n    text: country + ',' + capital };\nmsg.payload = csv.text;\nreturn msg;","outputs":1,"noerr":0,"x":1000,"y":440,"wires":[["8e65f041.e6908"]]},{"id":"1296359d.5eabea","type":"debug","z":"e7e549d4.4e7df8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":590,"y":520,"wires":[]},{"id":"c19e11b.c40b3f","type":"converter","z":"e7e549d4.4e7df8","name":"","from":"Shift_JIS","x":1020,"y":560,"wires":[["ce102314.8d30f8"]]},{"id":"ae77dcec.fa83e8","type":"split","z":"e7e549d4.4e7df8","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":610,"y":440,"wires":[["ce9e96a4.067db8"]]},{"id":"8e65f041.e6908","type":"join","z":"e7e549d4.4e7df8","name":"再配列化","mode":"custom","build":"string","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"num","reduceFixup":"","x":840,"y":560,"wires":[["c19e11b.c40b3f","ba17a3ed.820de"]]},{"id":"ce9e96a4.067db8","type":"sort","z":"e7e549d4.4e7df8","name":"","order":"ascending","as_num":false,"target":"","targetType":"seq","msgKey":"","msgKeyType":"elem","seqKey":"payload.country","seqKeyType":"msg","x":750,"y":440,"wires":[["40b5706f.2cef1"]]},{"id":"ba17a3ed.820de","type":"debug","z":"e7e549d4.4e7df8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1000,"y":640,"wires":[]}]
```