【GAS】スプレッドシートからkintoneのフィールドを登録する

## やりたいこと
大量のフィールドをアプリに登録するのがめんどくさいので、スプレッドシートにフィールド情報を入力してkintoneにフィールドを登録＆配置させたい。
※[利用するAPI](https://developer.cybozu.io/hc/ja/articles/204529724#anchor_changeform_changelayout)

## おおまかな流れ
1. 空のアプリを作る
2. APIトークンを生成（アクセス権はアプリ管理だけでOK）
3. スプレッドシートにフィールド名やらフィールドコードとかを入力する
4. スプレッドシートに入力された情報を元にkintoneにフィールドを登録する
5. スプレッドシートに入力された情報を元にフィールドを配置する

## 注意
:::note warn
↓のフィールドは対象外。ルックアップとかは設定が多すぎてめんどくさくなった...そのうち作るかも
レコード番号とかは配置はできそうだけどめんどくさくなった...そのうち作るかも
・レコード番号、作成者、更新者、作成日時、更新日時、ルックアップ、関連レコード、サブテーブル
:::
:::note warn
とりあえずフィールドを置く作業を軽減したかったのでフィールド設定の更新には対応してないです。
:::
:::note warn
急ぎで動くものがほしかったのでバリデーションは甘々です。
:::

## スプレッドシートの構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205237/a4641789-87a7-3162-90a8-b1b56bb6b145.png)
A列：ここに入力した数字でレイアウトを調整※
D列：対象外以外のフィールドで入力規制
E列：true or false
F列：true or false

※行1が↓に該当します。行数は昇順にしか対応してないので1,10,8とかで入力されると想定どおりに動かないです。すみません...
![スクリーンショット 2022-07-19 222053.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205237/de5413e7-f706-1d25-eef7-6b70c8377969.png)


## コード全体
```sample.js
const main = (data) => {
  try{
    const ss = SpreadsheetApp.getActive();
    const sheet = ss.getActiveSheet();
    const columnLength = sheet.getMaxColumns();
    const lastRow = sheet.getLastRow();
    const values = sheet.getRange(2, 1, lastRow - 1, columnLength).getValues();
    const apiToken = data.token;
    const appId = data.appId;
    const domain = data.domain;
    const fieldsBody = getFieldsBody(appId, values);
    const layoutBody = getLayoutBody(appId, values);
    const options = {
      "method": "post",
      "contentType": "application/json",
      "headers": {
        "X-Cybozu-API-Token": apiToken
      },
      "payload": JSON.stringify(fieldsBody)
    };
    const fetchFieldsUrl = `https://${domain}.cybozu.com/k/v1/preview/app/form/fields.json`;
    UrlFetchApp.fetch(fetchFieldsUrl,options);
    options.method = "put";
    options.payload = JSON.stringify(layoutBody);
    const fetchLayoutUrl = `https://${domain}.cybozu.com/k/v1/preview/app/form/layout.json`;
    UrlFetchApp.fetch(fetchLayoutUrl,options);
    Browser.msgBox(`処理が完了しました。設定画面で結果を確認して下さい。https://${domain}.cybozu.com/k/admin/app/flow?app=${appId}#section=form`);
  } catch (err) {
    Browser.msgBox(err)
    console.log(err);
  }
}

const getFieldsBody = (appId, values) => {
  const fieldsBody = {
    "app": appId,
    "properties": {}
  }

  values.forEach(e => {
    const fieldLabel = e[1];
    const fieldCode = e[2];
    const fieldType = e[3];
    const required = e[4] !== "" ? e[4] : false;
    const unique = e[5] !== "" ? e[5] : false;

    fieldsBody.properties[fieldCode] = {
      "type": fieldType,
      "code": fieldCode,
      "label": fieldLabel,
      "required": required,
      "unique": unique,
    }
    if (e[6] !== "") {
      const options = e[6].split(',');
      const optionsObj = {}
      for (let i = 0; i < options.length; i++) {
          optionsObj[options[i]] = {
              "label": options[i],
              "index": i
          }
      }
      fieldsBody.properties[fieldCode].options = optionsObj;
    }
    if (fieldType === "LINK") {
      if (e[7] === "") {
        throw new Error(`フィールドコード${fieldCode}にリンクタイプを指定してください。`)
      }
      fieldsBody.properties[fieldCode]["protocol"] = e[7];
    }
    if (fieldType === "CALC") {
      if (e[8] === "") {
        throw new Error(`フィールドコード${fieldCode}に計算式を指定してください。`)
      }
      fieldsBody.properties[fieldCode]["expression"] = e[8];
    }
  });
  return fieldsBody;
}

const getLayoutBody = (appId, values) => {
    const layoutBody = {
        "app": appId,
        "layout": []
    }
    const rows = [];
    values.forEach(e => {
        const rowValue = e[0];
        const fieldCode = e[2];
        const fieldType = e[3];
        if (rows.indexOf(rowValue) === -1) {
            rows.push(rowValue);
            layoutBody.layout.push({
                "type": "ROW",
                "fields": [
                    {
                        "type": fieldType,
                        "code": fieldCode
                    }
                ]
            })
        } else {
            const index = rows.indexOf(rowValue);
            layoutBody.layout[index].fields.push({
                "type": fieldType,
                "code": fieldCode
            })
        }
    });
    return layoutBody;
}
```

## コードの補足
``` sample.js
const main = (data) => {
  try{
    const ss = SpreadsheetApp.getActive();
    const sheet = ss.getActiveSheet();
    const columnLength = sheet.getMaxColumns();
    const lastRow = sheet.getLastRow();
    const values = sheet.getRange(2, 1, lastRow - 1, columnLength).getValues();
    const apiToken = data.token;
    const appId = data.appId;
    const domain = data.domain; // https://〇〇〇〇.cybozu.comの〇〇〇〇部分だけ
```
mainの引数のdataは↓のダイアログに入力したものが入ります。コードに直接情報を記載してもらっても問題ないです。
ダイアログの作り方は [こちら](https://tonari-it.com/gas-dialog-basic/) の記事を参考にしてます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205237/8d898b9e-0e82-698e-ab57-91121a1fdabc.png)

``` sample.js
const fieldsBody = getFieldsBody(appId, values);
```
getFieldsBody メソッドの処理でフィールド登録用のJSONを作成

``` sample.js
const layoutBody = getLayoutBody(appId, values);
```
layoutBody メソッドでフィールドの配置を設定するためのJSONを作成

``` sample.js
UrlFetchApp.fetch(fetchFieldsUrl,options);
```
これでまずフィールドをアプリに登録する。

``` sample.js
options.method = "put";
options.payload = JSON.stringify(layoutBody);
const fetchLayoutUrl = `https://${domain}.cybozu.com/k/v1/preview/app/form/layout.json`;
UrlFetchApp.fetch(fetchLayoutUrl,options);

```
methodとpayloadの中身を更新して、配置を設定するリクエストを送る