モデルにCSV出力のremote methodを付けてみた。
- Streamな感じに出力したい
- シフトJISな文字コードで出力したい
という点はおおむね達成できたが、csvと組み合わせるとうまく動作しなかったのが心残りだ。
StreamとシフトJIS変換はiconv-liteさまさま。さらにcsvなモジュールでstreamにpipeでうまく処理できるとパーフェクトなのだが。
'use strict';
const iconv = require('iconv-lite');
module.exports = function(Note) {
// サンプル用にModelのfindの代わり
function find(filter, cb) {
cb(null, [
{id: 1, date: new Date(2017, 4, 26), name: 'ラーメン', price: 600},
{id: 2, date: new Date(2017, 4, 27), name: 'カツ丼', price: 700},
{id: 3, date: new Date(2017, 4, 28), name: '唐揚げ定食', price: 800},
]);
}
Note.csv = function(filter, cb) {
find(filter, function(err, list) {
if (err) return cb(err);
const stream = iconv.encodeStream('Shift_JIS');
cb(null,
stream,
'text/csv; charset=shift_jis',
'attachment; filename=note.csv'
);
for (const item of list) {
const line = ['id', 'date', 'name', 'price'].map(function(key) {
return item[key];
}).join(',') + '\n';
stream.write(line);
}
stream.end();
});
};
Note.remoteMethod('csv', {
accepts: {arg: 'filter', type: 'string', http: {source: 'query'}},
returns: [
{arg: 'body', type: 'file', root: true},
{arg: 'Content-Type', type: 'string', http: {target: 'header'}},
{arg: 'Content-Disposition', type: 'string', http: {target: 'header'}},
],
});
};
remote methodをLoopBack API Explorerからたたくと、charset付けていてもutf-8に変換されるのは仕様なのだろうか?
HTMLファイルにform作ってpostしてみると、こちらからはちゃんとシフトJISでダウンロード出来るので良しとする。
日本語のファイル名でダウンロードさせたい場合
「日本語.csv」のようなファイル名は、レスポンスヘッダを少し変えて、次のようにする。
Content-Disposition: attachment; filename*=utf-8''%E6%97%A5%E6%9C%AC%E8%AA%9E.csv
filename=ファイル名はブラウザによって対応方法が異なるため、
"attachment; filename*=utf-8''" + encodeURIComponent('日本語.csv')
と、filename*=utf-8''URLエンコードしたファイル名を付けると良さそうです。
モデルのリレーション先も含める場合
findメソッドで、filter引数のincludeプロパティを使うとリレーションも含めた結果がもらえる。
※ただし、結果のリレーション部分のプロパティはFunctionで、もうひと手間toJSON()をかけてやらないとアクセスできないので注意。
参考:https://loopback.io/doc/en/lb3/Include-filter.html#access-included-objects
より汎用的に
モデルのdefinition.rawPropertiesを参照すると、汎用的な処理にできそうです。
mixinにしてしまえば、他のモデルへのCSV出力API追加もさくっと対応できますね。