kintone JS SDKを使ってjsとcssをダウンロードする

# はじめに
この記事は kintone Advent Calender 2019 の10日目です。
https://qiita.com/advent-calendar/2019/kintone
https://qiita.com/advent-calendar/2019/kintone2

本記事では、タイトルどおり[kintone JS SDK](https://github.com/kintone/kintone-js-sdk)を使ってjsとcssをアプリからダウンロードする処理を紹介します。

# なぜダウンロードしたいのか？
kintoneにてjsやcssを使って様々なカスタマイズを施しているケースでは、以下のようなニーズがあります。

- プラグインJSEdit for kintoneでjsを変更していたので最新版が手元に無い
- 2つのアプリで使用しているjsが同一なのか比較検証したい

などなど。このため、これらのファイルをローカルにダウンロードするという手間がしばしば発生しています。

# どうやってダウンロードするか？
アプリ内のカスタマイズページで手動ダウンロードは可能ですが、ファイルが複数だったり、いくつかのアプリから一括でダウンロードしたい場合に面倒なので、CLIでダウンロードできたほうが嬉しい。方法はいくつかありcli-kintoneだとコードを書かなくても実現できそうに思えるものの、諸般の事情により今回はkintone JS SDKを使ってダウンロードすることにしました。

# ダウンロード時の処理の流れと課題
kintoneアプリケーション内のjs/cssファイルをダウンロードする場合、以下のステップを踏むことになります。

1. kintoneの認証を行う
2. アプリケーションIDを引数として、js/cssのカスタマイズ設定を参照する
3. 参照した設定からfileKey等ダウンロードに必要なプロパティを取得する
4. 取得したfileKeyを引数として、ダウンロード処理を行う

しかし、kintone JS SDKでは2019/12現在、カスタマイズ設定を参照する処理はありません。なので、その処理はこちらで作ることにしました。JS SDKはkintoneREST APIを簡単に扱うためのものなので、そのものズバリなメソッドが用意されていなくても気軽に追加できるようです。

# 実装
## kintoneへの認証
kintone JS SDKでは、認証方法としてアイパス方式とAPIトークン方式のどちらも提供されています。今回は、普段の運用でデプロイツール「[ginue](https://github.com/goqoo-on-kintone/ginue)」を使っていることもあって、`.netrc` を読み出してアイパス方式で処理することにしました。 `.netrc` については[こちら](https://qiita.com/r-tamura/items/c6e49a3eb7f7f8aafb9d)の記事が詳しいです。

```js
//.netrcを読み出すためのパッケージ
const { default: netrc } = require('netrc-parser');
const kintone = require('@kintone/kintone-js-sdk');

const init = (domain, type = 'app') => {
    //.netrcではmachinesで区切ることでドメイン名ごとのアイパスを格納できるので、
    //アイパスを取り出す場合は、netrcをロードしたうえで、ドメインを指定して取り出す
    netrc.loadSync();
    const auth = netrc.machines[domain];

    const kintoneAuth = new kintone.Auth();
    kintoneAuth.setPasswordAuth({username: auth.login, password: auth.password});
    const kintoneConnection = new kintone.Connection({domain: domain, auth: kintoneAuth});

    //JS SDKでは、使用するメソッドによってインスタンスが異なるので、typeで返すインスタンスを切り替える
    return type === 'file' ? new kintone.File({connection: kintoneConnection}) : new kintone.App({connection: kintoneConnection});
}
```
以上のようにしておけば、認証情報をセットしたインスタンスが返るので、JS SDKのメソッドを実行したいときには、冒頭で`init()`を実行するだけとなります。

## js/cssのカスタマイズ設定の取得
次にカスタマイズ設定を取得します。すでに述べたとおり、カスタマイズ設定を取得するメソッドは無いので、 他のメソッドに倣って`kintone.App.sendRequest()`を呼び出すことになります。なお`kintone.App.sendRequest()`はPromiseを返すので、async/awaitを使うことにします。

```js
const getCustomize = async (domain, id, isPreview = false) => {
    //ここで先程のinit()を呼び出す
    const kintoneApp = init(domain);

    //他のSDK内のメソッドに習ってisPreviewでURLを切り替えるようにしておくが、今回は利用しない
    const url = isPreview ?
        `https://${domain}/k/v1/preview/app/customize.json`
      : `https://${domain}/k/v1/app/customize.json`;

    return await kintoneApp.sendRequest('GET', url, {app: id});
}
```

## fileKeyの取得
`getCustomeize()`から返ってくるjsonを元にfileKeyを素直に取り出そうとすると、ネストしたループ処理を書く羽目になってしまうのが嫌なので、`getAssets()`で一工夫しています。また、fileKeyだけ返すと、ダウンロード時のファイル名を指定できなくなるので、fileプロパティを丸ごと取得しておきます。

```js
const getAssets = (customize, ext) => {
    const desktop = customize['desktop'][ext] || [];
    const mobile = customize['mobile'][ext] || [];

    return desktop.concat(mobile);
}

const getDownloadItems = async (domain, id) => {
    const customize = await getCustomize(domain, id);
    const js = getAssets(customize, 'js');
    const css = getAssets(customize, 'css');
    const assets = js.concat(css).filter(asset => asset.type === 'FILE');

    return assets.map(asset => asset.file);
};
```

## ダウンロード
最後に取り出したfileKeyの配列をSDKの`kintone.File.download()`に渡して処理します。また、そのままダウンロードすると実行モジュールと同じ場所に、ファイルがダウンロードされてしまい面倒なので、`make-dir`を使ってidを名前とするディレクトリを作成しておきます。

```js
const makeDir = require('make-dir');

const download = async (domain, id) => {
    try {
        const items = await getDownloadItems(domain, id);

        if (items.length === 0) {
            console.log(`${domain}/k/${id}にはダウンロード対象のファイルは存在しません`);
            return;
        }

        const downloadPath = `./${id}`;
        await makeDir(downloadPath);

        kintoneFile = init(domain, 'file');
        items.map(async item => await kintoneFile.download({fileKey: item.fileKey, outPutFilePath: `${downloadPath}/${item.name}`}));
    } catch(e) {
        console.error(`[error!]${e.message}`);
        return;
    }

    console.log(`${domain}/k/${id}からファイルをダウンロードしました`);
}

appId = 1;
download('hoge.cybozu.com', appId);
```
以上です。

ちなみに実際の運用時には、コマンド実行時の引数にドメイン名を指定したり、`.ginuerc`のキー名からIDを引いたりもう少しカスタマイズしています。更にダウンロード後にchecksumを生成して、2アプリケーション間のファイル差分をチェックなど、用途に応じて様々な処理を組み合わせたりもしています。

# おわりに
実は当初、非常にお世話になっているE2Eテスト用の[goqoo-on-kintone/jinzo-ningen](https://github.com/goqoo-on-kintone/jinzo-ningen)というパッケージを紹介する話を書いていましたが、12/12のアドベントカレンダーで作者様自らE2Eテストについて書くと宣言されているので、そちらはボツにして今回の記事を急遽書くことにしました。

kintone開発は(個人的に)いろいろ苦労があったものの、公式パッケージだけでなく上記ginueやjinzo-ningenのようなパッケージのおかげで、満足できるレベルまで環境を整えることができたと感じています。これからもkintoneに関するノウハウが蓄積され、より良い環境が整っていくことを願っています。