kintone で他のアプリのレコードを更新するのと、添付ファイルをコピーするカスタマイズ

今回は、他のアプリのレコードを更新するカスタマイズです。
レコードがなければ登録・既にレコードがあれば更新という処理をしたいと思います。
※こういう、無ければ登録、あれば更新する処理のことをUPSERT（アップサート）といいます。

要は、レコードを別アプリにコピーしようというカスタマイズです。
最初、記事を書いているときに添付ファイルが絡んでくるのを忘れていて、最終的に長い記事になってしまいましたが、
ついでに添付ファイルのコピー手順もこうするんだーって思っていただけたらと思います👀💦

# アプリの準備

アプリは、登録・更新される側のアプリと操作側のアプリの2つを追加します。

## アプリ1つ目（登録・更新される側）
kintoneアプリストアから、「顧客リスト」を追加して、「顧客ID」フィールドを追加します。

| フィールド種類 | フィールドコード | 備考 |
|:-:|:-:|:-:|
| 文字列(1行)  | 顧客ID  | **重複禁止**  |

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/7384062c-8b4b-5332-ecf1-cc986dff8d3e.png)

## アプリ2つ目（操作用）
アプリ1つ目を再利用して作成しましょう。

アプリ名は「顧客リスト-操作用」などとつけておきましょう。
今回はこのアプリ2つ目の方をカスタマイズしていきます。

|登録更新される側 | 操作側 |
|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/f5741fb8-f153-9ee9-6ce4-a2fef41c8282.png)  |  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/0fc5fe16-8c83-4fae-ba9c-06392be4cf9d.png) |



# JavaScript 準備
**kintone REST API Client**と**kintone UI Component** を使います。
操作側のアプリの方の`JavaScript / CSSでカスタマイズ`に設定しておきましょう。

👀 **kintone REST API Client**
CDN:
`https://unpkg.com/@kintone/rest-api-client@latest/umd/KintoneRestAPIClient.js`

https://github.com/kintone/js-sdk/tree/master/packages/rest-api-client

👀 **kintone UI Component**
CDN:
`https://unpkg.com/kintone-ui-component/umd/kuc.min.js`

https://kintone-ui-component.netlify.app/docs/getting-started/quick-start

# JavaScriptを書く

試行錯誤しながら書いていきましょう👀

## 👀　まず、レコード詳細画面・・・・

```js
// レコード詳細画面、
kintone.events.on(["app.record.detail.show"], (event) => {
  
});
```

## 🤔・・・に、ボタンを設置します。
取り急ぎ「ボタン」でいいかな。
kintone UI Component を使ってお手軽ボタン設置です。楽ちん。

```js
// レコード詳細画面、
kintone.events.on(["app.record.detail.show"], (event) => {
  // 別アプリにupsertするボタンを設置
  const sp = kintone.app.record.getHeaderMenuSpaceElement();
  const record = event.record;
  const btnA = new Kuc.Button({
    text: "ボタン",
    type: "submit",
  });
  sp?.appendChild(btnA);

});
```

## 🙄それから、「ボタンをクリックしたとき」を追加します。

```js
// レコード詳細画面、
kintone.events.on(["app.record.detail.show"], (event) => {
  // 別アプリにupsertするボタンを設置
  const sp = kintone.app.record.getHeaderMenuSpaceElement();
  const record = event.record;
  const btnA = new Kuc.Button({
    text: "ボタン",
    type: "submit",
  });
  sp?.appendChild(btnA);

  // ボタンクリックしたらupsert
  btnA.addEventListener("click", async () => {
    // upsertする処理をここに書く
  });
});
```


顧客IDが同じレコードを更新すれば良い（同じ顧客IDのレコードが無ければINSERT,つまりUPSERT）ので、
こんな感じかな・・・・

```js
// レコード詳細画面、
kintone.events.on(["app.record.detail.show"], (event) => {
  // 別アプリにupsertするボタンを設置
  ～省略～

  // ボタンクリックしたらupsert
  btnA.addEventListener("click", async () => {
    // upsert する
    const client = new KintoneRestAPIClient();
    try {
      const res = await client.record.upsertRecord({
        app: 【登録更新される側のアプリID】,
        updateKey: {
          field: "顧客ID",
          value: record.顧客ID.value,
        },
        record: record,
      });
      console.log(res);
      alert("upsert成功！");
    } catch (err) {
      alert("upsert失敗！！");
      console.log(err);
    }
  });
});
```

## 🤣しかしコレだと怒られます。何がダメかって言うと、

更新しようとしている record 内には余計なフィールドが含まれているからです。

```js
const res = await client.record.upsertRecord({
  app: 【登録更新される側のアプリID】,
  updateKey: {
    field: "顧客ID",
    value: record.顧客ID.value,
  },
  record: record, // ← record 内に余計なフィールドがたくさんある
});
```

## 🤔更新に余計なフィールドとは？

[制限事項](https://developer.cybozu.io/hc/ja/articles/201941784#step4)のあたりを参考にしたり、試してみたりした結果、
upsertRecordメソッドに渡す record からは以下のフィールドを削除しましょう。

* 顧客ID（updateKeyに設定する場合はrecordに含んではいけない）
* $id
* $revision
* レコード番号
* 作成者
* 更新者
* 作成日時
* 更新日時


```js
// レコード詳細画面、
kintone.events.on(["app.record.detail.show"], (event) => {
  ～省略

  // ボタンクリックしたらupsert
  btnA.addEventListener("click", async () => {
    // upsertに必要なキーを避難しておく
    const uKey = record.顧客ID.value;
    // upsertに不要なフィールドを削除
    delete record.顧客ID;
    delete record.$id;
    delete record.$revision;
    delete record.レコード番号;
    delete record.作成者;
    delete record.更新者;
    delete record.作成日時;
    delete record.更新日時;

    // upsert する
    const client = new KintoneRestAPIClient();
    try {
      const res = await client.record.upsertRecord({
        app: 【登録更新される側のアプリID】,
        updateKey: {
          field: "顧客ID",
          value: uKey,  //← record.顧客IDは削除してしまっているので避難させたuKeyをここで使う
        },
        record: record,
      });
      console.log(res);
      alert("upsert成功！");
    } catch (err) {
      alert("upsert失敗！！");
      console.log(err);
    }
  });
});
```

## 👀一見できた！ようにみえるけど・・・

これだと、添付ファイルがあるレコードをコピーしようとすると怒られてしまいます。
※顧客リストアプリだと「会社ロゴ」が添付ファイルフィールドにあたります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536148/5aa8f7f9-2133-00f9-342f-5773bde24099.png)

原因は、添付ファイルの・・・キー！！
添付ファイルフィールドにはダウンロード用のキーが格納されていますが、
登録や更新の場合は「アップロード用のキー」である必要があります。

## 😎添付ファイルをコピーする手順

添付ファイルをダウンロードして、アップロード・・・（つまりコピー）するには次のような手順を踏まないといけません。

1. ダウンロードキーからファイルをダウンロードする
1. ダウンロードしたファイルをアップロードするしてアップロードキーを取得する
1. アップロードキーをレコードに登録する

コレを素直にやろうとすると、
コチラの記事2つをコラボレーションさせて仕組みを作らなくてはいけません👀💦

* [ファイルダウンロードで必須となる2つの手順](https://developer.cybozu.io/hc/ja/articles/200814380)
* [ファイルアップロードで必須となる3つの手順](https://developer.cybozu.io/hc/ja/articles/200724665)

（´-`）.｡oO（さていっちょやってみるか～～）

と思ったけど、生の kintone REST API を自在に扱う能力が筆者にはありません。
ここは素直にkintone REST API Client にお世話になりましょう。


### 😁添付されているファイルが1個だけのとき

ええと🙄、recordの会社ロゴのvalueは配列で、
ファイルは1個添付されてて、
てことは0番目にfileKeyプロパティがあるので、
ダウンロードファイルキーをアップロードファイルキーに変えるコードはこんな感じかな。
※recordとは、`const record = event.record;` したrecordから、余計なフィールドを取り除いたものです。


```js
// kintone REST API Client の力を借りる
const client = new KintoneRestAPIClient();

// 添付ファイルフィールドのダウンロードキーからblobを作成
const blob = await client.file.downloadFile({
  fileKey: record.会社ロゴ.value[0].fileKey, // ダウンロードファイルキー
});

// アップロードするファイルを作成
const FILE = {
  name: record.会社ロゴ.value[0].name, // ファイル名
  data: blob,
};

// アップロードしてアップロードキーを取得する
const ulFileKey = await client.file.uploadFile({
  file: FILE,
});

// レコードのダウンロードキーをアップロードキーに書き換える
record.会社ロゴ.value[0].fileKey = ulFileKey.fileKey;
```

### 🤔添付されているファイルが複数！のとき

さっきのはファイルが1個添付されている時。複数個に対応したいナ。
そんなときは、`for...of`が楽かな（forEachはasync/awaitが使えない）

```js
// kintone REST API Client の力を借りる
const client = new KintoneRestAPIClient();
// filesにファイルの配列を入れる。
const files = record.会社ロゴ.value;

// 添付ファイル数の分繰り返す
for (const f of files) {
  // 添付ファイルフィールドのダウンロードキーからblobを作成
  const blob = await client.file.downloadFile({
    fileKey: f.fileKey, // ダウンロードファイルキー
  });

  // アップロードするファイルを作成
  const FILE = {
    name: f.name, // ファイル名
    data: blob,
  };

  // アップロードしてアップロードキーを取得する
  const ulFileKey = await client.file.uploadFile({
    file: FILE,
  });

  // レコードのダウンロードキーをアップロードキーに書き換える
  f.fileKey = ulFileKey.fileKey;
}
```

### 💡コレを関数にしよう

とりあえず、recordとフィールド名を入れたらダウンロードキーをアップロードキーに書き換えてくれる関数にしよう。
添付ファイルが1つもなかったら処理せずに終わろう。
とするとこんな感じかな。
これで、フィールド名の配列を入れられるような関数を作るのもありかも。

```js
// ダウンロードキーからファイル取得＆アップロードしてアップロードキーに書き換える
const dlkey2upkey = async (record, field) => {
  const files = record[field].value; // fieldは変数なのでブラケット記法で書こう
  if (files.length === 0) {
    // 添付ファイルなかったらこれやらない
    return;
  }

  // kintone REST API Client の力を借りる
  const client = new KintoneRestAPIClient();

  // 添付ファイル数の分繰り返す
  for (const f of files) {
    // 添付ファイルフィールドのダウンロードキーからblobを作成
    const blob = await client.file.downloadFile({
      fileKey: f.fileKey, // ダウンロードファイルキー
    });

    // アップロードするファイルを作成
    const FILE = {
      name: f.name, // ファイル名
      data: blob,
    };

    // アップロードしてアップロードキーを取得する
    const ulFileKey = await client.file.uploadFile({
      file: FILE,
    });

    // レコードのダウンロードキーをアップロードキーに書き換える
    f.fileKey = ulFileKey.fileKey;
  }
};
```

## 最終的なコードはこんな感じ

こんな感じになると思います。


```js
(() => {
  // レコード詳細画面、
  kintone.events.on(["app.record.detail.show"], (event) => {
    // recordはここで宣言しとこう
    const record = event.record;

    // 別アプリにupsertするボタンを設置
    const sp = kintone.app.record.getHeaderMenuSpaceElement();
    const btnA = new Kuc.Button({
      text: "ボタン",
      type: "submit",
    });
    sp?.appendChild(btnA);

    btnA.addEventListener("click", async () => {
      // upsertに必要なキーを避難
      const uKey = record.顧客ID.value;
      // upsertに不要なフィールドを削除
      delete record.顧客ID;
      delete record.$id;
      delete record.$revision;
      delete record.レコード番号;
      delete record.作成者;
      delete record.更新者;
      delete record.作成日時;
      delete record.更新日時;

      // 添付ファイルフィールドのダウンロードキーをアップロードキーに変える
      await dlkey2upkey(record, "会社ロゴ");

      // upsert する
      const client = new KintoneRestAPIClient();
      try {
        const res = await client.record.upsertRecord({
          app: 【登録更新される側のアプリID】,
          updateKey: {
            field: "顧客ID",
            value: uKey,
          },
          record: record,
        });
        alert("upsert成功！");
        console.log(res);
      } catch (err) {
        alert("upsert失敗！！");
        console.log(err);
      }
    });
  });

  // ダウンロードキーからファイル取得＆アップロードしてアップロードキーに書き換える
  const dlkey2upkey = async (record, field) => {
    const files = record[field].value;
    if (files.length === 0) {
      // 添付ファイルなかったらこれやらない
      return;
    }

    // kintone REST API Client の力を借りる
    const client = new KintoneRestAPIClient();

    // 添付ファイル数の分繰り返す
    for (const f of files) {
      // 添付ファイルフィールドのダウンロードキーからblobを作成
      const blob = await client.file.downloadFile({
        fileKey: f.fileKey, // ダウンロードファイルキー
      });

      // アップロードするファイルを作成
      const FILE = {
        name: f.name, // ファイル名
        data: blob,
      };

      // アップロードしてアップロードキーを取得する
      const ulFileKey = await client.file.uploadFile({
        file: FILE,
      });

      // レコードのダウンロードキーをアップロードキーに書き換える
      f.fileKey = ulFileKey.fileKey;
    }
  };
})();

```

# まとめ
今回はレコードを他のアプリにコピーするカスタマイズをしてみました。
同じアプリを再利用して作ったので、upsertにわたすパラメータを作るのは楽でしたが、
kintone REST API では慣れるまではそのパラメータをどう組み立てるかがミソというか苦労ポイントだと思います。
色々試行錯誤しながら書けるようになってもらえば～と思います👀💦