kintone とGoogle Apps Script連携

# Google Apps Scriptからkintone APIを呼ぶ
kintoneの標準機能だけで補いきれない部分をGASで補うと便利。
以下のような用途で便利。

* 帳票を作成
* 自動メール送信
* 定期実行バッチ処理

# コード

```js:kintone_manager.gs
var KintoneManager = (function() {
    "use strict";
    // user, passが指定されれば、パスワード認証
    // 指定されなければ、APIトークン認証
    // appsは以下の形式
    // {
    //    // アプリケーション名はkintoneのデータに依存せず、GAS内のコードで取り扱う専用
    //    YOUR_APP_NAME1: {
    //       appid: 1,
    //       guestid: 2,
    //       name: "日報",
    //       token: "XXXXXXXXXXXXXX_YOUR_TOKEN_XXXXXXXXXXXXXX" // パスワード認証する場合は省略化
    //    },
    //    YOUR_APP_NAME2: {
    //       ...
    //    }
    // }
    function KintoneManager(subdomain, apps, user, pass){
        this.subdomain = subdomain;
        this.authorization = null;
        this.apps = apps;

        if (arguments.length > 3) {
            this.authorization = Utilities.base64Encode(user + ":" + pass);
        } else if (arguments.length > 2) {
            // 引数が3つの場合はエンコード済みの認証情報として処理
            this.authorization = user;
        }
    }
    // レコードの作成
    KintoneManager.prototype.create = function(app_name, records) {
        var app = this.apps[app_name];
        var payload = {
          app: app.appid,
          records: records
        };
        var response = UrlFetchApp.fetch(
            "@1/records.json".replace(/@1/g,this._getEndpoint(app.guestid)),
           this._postOption(app, payload)
        );
        return response;
    };
    // レコードの検索
    KintoneManager.prototype.search = function(app_name, query){
       var q = encodeURIComponent(query);
       var app = this.apps[app_name];
       var response = UrlFetchApp.fetch(
         "@1/records.json?app=@2&query=@3"
            .replace(/@1/g, this._getEndpoint(app.guestid))
            .replace(/@2/g, app.appid)
            .replace(/@3/g, q),
         this._getOption(app)
       );
       return response;
    };
    // レコードの更新
    KintoneManager.prototype.update = function(app_name, records) {
        var app = this.apps[app_name];
        var payload = {
          app: app.appid,
          records: records
        };
        var response = UrlFetchApp.fetch(
            "@1/records.json".replace(/@1/g, this._getEndpoint(app.guestid)),
           this._putOption(app, payload)
        );
        return response;
    };
    // レコードの削除
    KintoneManager.prototype.destroy = function(app_name, record_ids){
       var app = this.apps[app_name];
       var query = "app=" + app.appid;
       for(var i=0; i<record_ids.length;i++){
           query += "&ids[@1]=@2".replace(/@1/g,i).replace(/@2/g,record_ids[i]);
       }
       var response = UrlFetchApp.fetch(
         "@1/records.json?@2"
            .replace(/@1/g, this._getEndpoint(app.guestid))
            .replace(/@2/g, query),
         this._deleteOption(app)
       );
       return response;
    };
    // GETメソッドの時のオプション情報
    KintoneManager.prototype._getOption = function(app) {
       var option = {
          method: "get",
          headers: this._authorizationHeader(app),
          muteHttpExceptions: true
       };
       return option;
    };
    // POSTメソッドの時のオプション情報
    KintoneManager.prototype._postOption = function(app,payload) {
       var option = {
               method: "post",
               contentType: "application/json",
               headers: this._authorizationHeader(app),
               muteHttpExceptions: true,
               payload: JSON.stringify(payload)
       };
       return option;
    };
    // PUTメソッドの時のオプション情報
    KintoneManager.prototype._putOption = function(app,payload) {
       var option = {
               method: "put",
               contentType: "application/json",
               headers: this._authorizationHeader(app),
               muteHttpExceptions: true,
               payload: JSON.stringify(payload)
       };
       return option;
    };
    // DELETEメソッドの時のオプション情報
    KintoneManager.prototype._deleteOption = function(app) {
       var option = {
          method: "delete",
          headers: this._authorizationHeader(app),
          muteHttpExceptions: true
       };
       return option;
    };
    // エンドポイントの取得
    KintoneManager.prototype._getEndpoint = function(guest_id) {
      var endpoint = "https://@1.cybozu.com".replace(/@1/g,this.subdomain);
      if (guest_id == null) {
        return endpoint + "/k/v1";
      } else {
        return endpoint + "/k/guest/@1/v1".replace(/@1/g, guest_id);
      }
    };
    // ヘッダーの認証情報
    KintoneManager.prototype._authorizationHeader = function(app) {
      if (this.authorization) {
         // パスワード認証
         return { "X-Cybozu-Authorization": this.authorization };
      } else if (app.token) {
         // APIトークン認証
         return { "X-Cybozu-API-Token": app.token };
      } else {
        throw new Error("kintone APIを呼ぶための認証情報がありません。");
      }
    };
    return KintoneManager;
})();
```

# 初期化
初期化時に扱うアプリの情報と認証情報を渡す。

### パスワード認証

アプリのAPIトークンは使わず、kintoneのユーザー名とパスワードを使うパターン
ゲストスペースのアプリの場合はguestidを下記のYOUR_APPLICATION2のように設定する。

```js
var subdomain = "YOUR_COMPANY";
var apps = {
  YOUR_APPLICATION1: { appid: 1, name: "アプリ１"},
  YOUR_APPLICATION2: { appid: 2, guestid: 1, name: "アプリ２"}
};
var user = "YOUR_KINTONE_USER_NAME";
var pass = "YOUR_KINTONE_PASSWORD";

var kintone_manager = new KintoneManager(subdomain, apps, user, pass);
```
### APIトークン認証

パスワードは使わず、各アプリのAPIトークンを設定
ルックアップなど[1つのアプリで複数のトークンが必要な場合](https://developer.cybozu.io/hc/ja/articles/360035992312-%E8%A4%87%E6%95%B0-API-%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%93%E3%81%A8)はカンマ区切りで指定

```js
var subdomain = "YOUR_COMPANY";
var apps = {
  YOUR_APPLICATION1: { appid: 1, name: "アプリ１", token: "YOUR_API_TOKEN1" },
  YOUR_APPLICATION2: { appid: 1, name: "アプリ２", token: "YOUR_API_TOKEN2-1,YOUR_API_TOKEN2-2"}
};

var kintone_manager = new KintoneManager(subdomain, apps);
```

# CREATE
recordsの形式の詳細は[公式ドキュメント](https://cybozudev.zendesk.com/hc/ja/articles/202166160-%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E7%99%BB%E9%8C%B2-POST-#step2)を参照してください。
responseで返ってくるのはGASのクラスである[HTTPResponseクラス](https://developers.google.com/apps-script/reference/url-fetch/http-response)です。

```js
var records = [
        {   // 1件目のデータ
            "(フィールドコード)": {
                "value": (フィールド値)
            },
            "(フィールドコード)": {
                "value": (フィールド値)
            },
            :
        },
        {   // 2件目のデータ
            "(フィールドコード)": {
                "value": (フィールド値)
            },
            "(フィールドコード)": {
                "value": (フィールド値)
            },
            :
        },
        :
        {   // n件目のデータ
            :
        }
];
var response = kintone_manager.create("YOUR_APPLICATION1", records);
// ステータスコード
// 成功すれば200になる
var code = response.getResponseCode();
```

# UPDATE
recordsの形式の詳細は[公式ドキュメント](https://cybozudev.zendesk.com/hc/ja/articles/201941784-%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E6%9B%B4%E6%96%B0-PUT-#step2)を参照してください。

```js
var records = [
        {   // 1件目のデータ
            "id": (更新するレコードのID),
            "revision": (更新するレコードのリビジョン),// 不要の場合は記載しない
            "record": {
                "(フィールドコード)": {
                    "value": (フィールド値)
                },
                "(フィールドコード)": {
                    "value": (フィールド値)
                }
            }
        },
        {   // 2件目のデータ
            "id": (更新するレコードのID),
            "revision": (更新するレコードのリビジョン),// 不要の場合は記載しない
            "record": {
                "(フィールドコード)": {
                    "value": (フィールド値)
                },
                "(フィールドコード)": {
                    "value": (フィールド値)
                }
            }
        },
       {   // n件目のデータ
            "id": (更新するレコードのID),
            "revision": (更新するレコードのリビジョン),// 不要の場合は記載しない
            "record": {
                "(フィールドコード)": {
                    "value": (フィールド値)
                },
                "(フィールドコード)": {
                    "value": (フィールド値)
                }
            }
        }
];
var response = kintone_manager.update("YOUR_APPLICATION1", records);
// ステータスコード
// 成功すれば200になる
var code = response.getResponseCode();
```
# SEARCH
クエリの書き方は[公式ドキュメント](https://cybozudev.zendesk.com/hc/ja/articles/202331474-%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E5%8F%96%E5%BE%97-GET-#step2)を参照してください。


```js
var query = 'property1 = "hoge"'
var response = kintone_manager.search("YOUR_APPLICATION1", query);
// ステータスコード
// 成功すれば200になる
var code = response.getResponseCode();
var content = JSON.parse(response.getContentText());
// レコードの配列が取得できる。
var records = content.records;

```
# DESTROY

```
// 削除するレコード番号の配列
var record_ids = [1,3,5];
kintone_manager.destroy("YOUR_APPLICATION1", record_ids);
// ステータスコード
// 成功すれば200になる
var code = response.getResponseCode();
```

# 制限
CREATEは1回100件まで、SEARCHは500件までなどのkintone APIの制限がありますので、制限を超える場合はループで回すなどの処理を適宜書いてください。

# 公開
公開してみました。
~~プロジェクト・キーは以下です。~~
~~`MDT2NQ9jkAGYJ-7ftp_A0v08CaFRWuzzx`~~

**2021/3/18 追記**
プロジェクト・キーの代わりにスクリプト・キーを使うようにG側で修正がありました。
~~下記のスクリプトキーを使用してください。~~
~~`1M8DHQSWWGOM2RSRjDZmylHMl03nZHnliaVMGMk84m6jQ05ditPBTCSM8`~~


**2022/1/31 追記**
原因不明なのですが、上記スクリプトキーでライブラリが使えなくなってしまいました。
新しく同じソースコードでプロジェクトを作り直しました。
下記のスクリプト・キーを使用してください。
`1UMIqaatccEb68gRI0rDbupn8UdF5nxMcQghQXzyOLyEHnqunUoIcpU5y`

下記スクショみたいな感じで導入できます。

### スクショ1
![first.png](https://qiita-image-store.s3.amazonaws.com/0/30931/04f46768-8c76-5313-aa37-2b5ed3de4a86.png)

### スクショ2
![second.png](https://qiita-image-store.s3.amazonaws.com/0/30931/7b52c904-2408-cda5-248b-517b8736125e.png)

### スクショ3
![third.png](https://qiita-image-store.s3.amazonaws.com/0/30931/0174e35d-3e71-eea6-8342-aa3044644d37.png)

## ライブラリ利用時の初期化
```js:
// パスワード認証
var manager = new KintoneManager.KintoneManager(subdomain, apps, user, password);
```
```js:
// APIトークン認証
var manager = new KintoneManager.KintoneManager(subdomain, apps);
```