kintone のアプリのデータの集計値を、別アプリに保管する（外部API連携）

## 処理内容
・kintone の明細アプリのデータを、Lambda でバッチ処理させて、集計アプリの年月集計を更新します。
・集計アプリに該当する年月のデータがある場合は、集計アプリのレコードを更新します。
・集計アプリに該当する年月のデータがない場合は、集計アプリのレコードを追加します。

## kintone の設定
・コードアプリを追加します。
![kintoneTTL01.png](https://qiita-image-store.s3.amazonaws.com/0/75892/61f2c7c5-cf26-816b-6d5b-73e892d4dcd0.png)
・集計アプリを追加します。
![kintoneTTL02.png](https://qiita-image-store.s3.amazonaws.com/0/75892/3706cd28-5a52-b3aa-5013-5a35bbb1bd63.png)
・明細アプリを追加します。
![kintoneTTL03.png](https://qiita-image-store.s3.amazonaws.com/0/75892/017009d9-6242-d8bf-5c06-dd369e78337a.png)
・集計後はこのようになります。

## AWS Lambda の設定
・今回は node.js 8.10 で Lambda 関数を追加します。
![kintoneTTL11.png](https://qiita-image-store.s3.amazonaws.com/0/75892/8c5f961d-a0c1-4e92-12be-ad8ba6344496.png)
・定時に実行するため、CloudWatch Events を利用します。
![kintoneTTL12.png](https://qiita-image-store.s3.amazonaws.com/0/75892/6f7d3f6e-78bb-dc0e-9066-8fd5f191cc7d.png)
・バッチ処理を実行するためのトリガーを cron もしくは rate 式で設定します。（今回は cron 式で設定。）
![kintoneTTL13.png](https://qiita-image-store.s3.amazonaws.com/0/75892/2fa51ea5-0de9-a8c1-583f-f26f6a713bb7.png)
・メモリは 256MB、実行時間は最大の15分で設定しました。（15分以上かかる処理の場合、注意が必要）
![kintoneTTL14.png](https://qiita-image-store.s3.amazonaws.com/0/75892/9691ee86-1638-1d1a-5e4a-b1ba4164495f.png)

その他 AWS や Lambda の設定方法については以下を参考ください。

AWS マネジメントコンソール の操作
https://docs.aws.amazon.com/ja_jp/awsconsolehelpdocs/latest/gsg/getting-started.html

今度こそ理解する！俺式Lambda入門
https://dev.classmethod.jp/cloud/aws/lambda-my-first-step/

AWS Lambda 関数を cron のように定期実行
https://www.qoosky.io/techs/89d52682fb

Rate または Cron を使用したスケジュール式
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html

Lambda の Node.js でもっといろんなパッケージを使いたいとき
https://tech-lab.sios.jp/archives/9017

## Lambda のコード
先ず明細データを読み込んで、集計データを処理します。
node.js では、API経由で明細データを読み込んだ後に集計を行うような同期処理の実装には注意が必要です。
node.js の同期処理については、以下を参照ください。

Promise, async, await がやっていること 
https://qiita.com/kerupani129/items/2619316d6ba0ccd7be6a

request-promise モジュールを使用して HTTP 通信を行う
https://maku77.github.io/nodejs/net/request-promise-module.html

```javascript:index.js
'use strict';

var request = require('request-promise');

var Domain     = "cybozu.com";
var Subdomain  = "SUBDOMAIN";
var Path       = "/k/v1/record.json";
var GetsPath   = "/k/v1/records.json";
var Protocol   = "https://";
var Url        = Protocol + Subdomain + '.' + Domain + Path;
var GetUrl     = Protocol + Subdomain + '.' + Domain + GetsPath;

var ItemAppId  = "KINTONE_ITEM_APP_ID";
var ItemToken  = "KINTONE_ITEM_APP_TOKEN";
var TotalAppId = "KINTONE_TOTAL_APP_ID";
var TotalToken = "KINTONE_TOTAL_APP_TOKEN";
var TotalAuth  = "KINTONE_AUTH_DATA";

exports.handler = async function(event, context, callback) {
    
    console.log('Function Start.');
    
    // 明細アプリの情報を取得(本来なら日付範囲とかを指定する筈)
    var url   = Protocol + Subdomain + '.' + Domain + GetsPath;
    var query = ' order by code asc, date desc';
    let itemRecords = await GetKintoneRecodes(request, ItemAppId, GetUrl, ItemToken, query);
    
    // 集計処理
    var beforeCode = "";
    var beforeYm   = 0;
    var amount     = 0;
    var json       = {};
    url = Protocol + Subdomain + '.' + Domain + Path;
    
    for(var i = 0; i < itemRecords.length; i++ ){
        var record = itemRecords[i];
        var ym = parseInt(record['date']['value'].substring(0, 4), 10) * 100 + parseInt(record['date']['value'].substring(5, 7), 10);
        if(record['code']['value'] != beforeCode || ym != beforeYm){
           if(beforeCode != ""){
               json = {
                    "ym"     : { "value" : beforeYm.toString() },
                    "code"   : { "value" : beforeCode.toString() },
                    "amount" : { "value" : amount },
               };
               await UpdateAmount(request, TotalAppId, TotalToken, json);
           }
           beforeCode = record['code']['value'];
           beforeYm   = ym;
           amount     = 0;
        }
        amount += parseInt(record['amount']['value'], 10);
    }
    
    if(beforeCode != ""){
        json = {
            "ym"     : { "value" : beforeYm.toString() },
            "code"   : { "value" : beforeCode.toString() },
            "amount" : { "value" : amount },
        };
        await UpdateAmount(request, TotalAppId, TotalToken, json);
    }
    console.log('Function end.');
    
    // 集計データを更新する
    async function UpdateAmount(request, appId, token, json) 
    {
         // 集計アプリの情報を取得
         var query = 'code = "' + json.code.value + '" and ym = "' + json.ym.value + '" order by code asc, ym desc';
         let totalRecord = await GetKintoneRecode(request, appId, GetUrl, token, query);
    
         // 集計値を反映
         if(totalRecord == null){
             await PostKintoneRecode(request, Url, appId, TotalAuth, json);
         }else{
             var id = totalRecord['レコード番号']['value'];
             await PutKintoneRecode(request, Url, appId, TotalAuth, id, json);
         }
    }
};

// kintone のデータを取得する
async function GetKintoneRecodes(request, appId, url, token, query) 
{
    try {
        var options = {
            url: url + '?app=' + appId + '&query=' + encodeURI(query),
            method: 'GET',
            headers: {
                'X-Cybozu-API-Token': token
            },
            json: true
        };
        let res = await request(options);
        return res.records;
    } catch (err) {
        console.error(JSON.stringify(err));
        return null;
    }
};

async function GetKintoneRecode(request, appId, url, token, query) 
{
    try {
        var options = {
            url: url + '?app=' + appId + '&query=' + encodeURI(query),
            method: 'GET',
            headers: {
                'X-Cybozu-API-Token': token
            },
            json: true
        };
        let res = await request(options);
        if(res.records.length > 0){ 
            return res.records[0]; 
        }
        return null;
    } catch (err) {
        console.error(JSON.stringify(err));
        return null;
    }
};

// kintone のデータを追加する（Lookup フィールドの更新は token でできないため Auth で行う）
async function PostKintoneRecode(request, url, appId, auth, json)
{
    try {
        var options = {
            url: url,
            method: 'POST',
            headers: {
                'Content-type': 'application/json',
                'X-Cybozu-Authorization': auth
            },
            json: { app : appId, record: json },
        };
        await request(options);
        return true;
    } catch (err) {
        console.error(JSON.stringify(err));
        return false;
    }
}

// kintone のデータを更新する（Lookup フィールドの更新は token でできないため Auth で行う）
async function PutKintoneRecode(request, url, appId, auth, id, json)
{
    try {
        var options = {
            url: url,
            method: 'PUT',
            headers: {
                'Content-type': 'application/json',
                'X-Cybozu-Authorization': auth
            },
            json: { app : appId, id : id, record: json },
        };
        await request(options);
        return true;
    } catch (err) {
        console.error(JSON.stringify(err));
        return false;
    }
}

```

## 処理結果
・kintone の集計テーブルのレコードが以下のように追加・更新します。
![kintoneTTL04.png](https://qiita-image-store.s3.amazonaws.com/0/75892/6d313953-3491-2fa1-8171-4245362a4ffb.png)