[kintone×gas]スプレッドシートに入力した情報からフィールドを登録する_4

前回まで

↓の内容をやった。記事はこちら
・ダイアログに入力したデータを受け取れることを確認
・スプレッドシートの定義ファイル作成
・処理の大まかな流れを作成

リクエストボディを作る関数の実装

前回のコードの中で、getRequestBody 関数の中身を書いていく。

    // リクエストボディを作成する getRequestBody 関数に必要な情報を渡す。
    const [fieldRegistrationBody, layoutBody] = getRequestBody(appId, values, definitions);
const getRequestBody = (appId, values, definitions) => {
  const COLUMNS = definitions.COLUMNS;
  const FIELD_TYPES = definitions.FIELD_TYPES;
  // フィールド登録用のリクエストボディ
  const fieldRegistrationBody = {
    "app": appId,
    "properties": {}
  }
  // フィールドレイアウト更新用のリクエストボディ
  const layoutBody = {
    "app": appId,
    "layout": []
  }
  // 行番号(A列)の値で昇順にソート
  values.sort((a, b) => {
    return a[0] - b[0];
  });

  // サブテーブルの行番号とフィールドコードを代入する変数
  const subtableRowNoObj = {};
  // サブテーブル以外の行番号を代入する変数
  const normalFieldRowNo = [];
  // サブテーブル以外のフィールド情報を代入する変数
  const normalFieldValues = [];
  
  values.forEach(e => {
    // SSに入力されたフィールドコード(C列)を代入
    const fieldCode = e[COLUMNS.fieldCode];
    // SSに入力されたフィールドタイプ(D列)の情報を英語に変換して代入
    const fieldType = FIELD_TYPES[e[COLUMNS.fieldType]];

    if(fieldType === FIELD_TYPES.サブテーブル) {
      // サブテーブルの行番号が重複していないかチェック
      if(Object.keys(subtableRowNoObj).includes(String(e[COLUMNS.row]))) {
        throw new Error(`フィールドコード:${fieldCode}の行番号が重複しています。サブテーブルは行番号の重複ができません。`);
      }
      // フィールド登録用リクエストボディにサブテーブルの情報を追加
      fieldRegistrationBody.properties[fieldCode] = {
        "type": fieldType,
        "code": fieldCode,
        "label": e[COLUMNS.fieldLabel],
        "noLabel": e[COLUMNS.noLabel] === "" || e[COLUMNS.noLabel] === "false" ? false : true,
        "fields": {} // fieldsにはサブテーブル内のフィールド情報を追加していく
      }
      // サブテーブルの行番号とフィールドコードを代入
      subtableRowNoObj[String(e[COLUMNS.row])] = fieldCode;
      // レイアウト更新用リクエストボディにサブテーブルの情報を追加
      layoutBody.layout[e[COLUMNS.row] - 1] = {
        "type": FIELD_TYPES.サブテーブル,
        "code": fieldCode,
        "fields": [] // fieldsにはサブテーブル内のフィールド情報を追加していく
      }
    } else {
      // サブテーブル以外のフィールドの処理

      // classに定義した選択肢が必要か判定する関数でチェック。booleanで返却
      const needChoices = definitions.checkNeedOpitons(fieldType);
      // 選択肢が必要なフィールドでSSに入力がなければエラーを返す
      if (needChoices && e[COLUMNS.choices] === "") throw new Error(`フィールドコード:${fieldCode}は選択肢の入力が必要です`);
      // リンクフィールドでリンクタイプの指定がなければエラーを返す
      if (fieldType === FIELD_TYPES.リンク && e[COLUMNS.linkType] === "") throw new Error(`フィールドコード:${fieldCode}ははリンクタイプの指定が必要です。`);
      if (fieldType === FIELD_TYPES.計算 && e[COLUMNS.formula] === "") throw new Error(`フィールドコード:${fieldCode}は計算式が必要です。`);
      // サブテーブル以外のフィールドの情報を配列に追加
      normalFieldValues.push(e);
      // サブテーブル以外のフィールドの行番号を配列に追加。レイアウト用のリクエストボディ作成に利用
      normalFieldRowNo.push(String(e[COLUMNS.row]));
    }
  });

  // サブテーブル以外フィールドの行番号の重複を排除
  const uniqueNormalFieldRowNo = Array.from(new Set(normalFieldRowNo));
  // サブテーブルの行番号を配列で代入
  const subTblRowNo = Object.keys(subtableRowNoObj);
  // uniqueNormalFieldRowNo からサブテーブルと同じ行番号を排除する
  const rowNoWithoutSubTbl = uniqueNormalFieldRowNo.filter(i => subTblRowNo.indexOf(i) === -1);
  // サブテーブルに含まれないフィールドのレイアウト用リクエストボディ
  rowNoWithoutSubTbl.forEach(e => {
    layoutBody.layout[e - 1] = {
      "type": "ROW",
      "fields": []
    }
  });

  // 通常フィールドのリクエストボディを作成
  normalFieldValues.forEach(e => {
    const fieldCode = e[COLUMNS.fieldCode];
    const fieldType = FIELD_TYPES[e[COLUMNS.fieldType]];
    const requestBody = {
      "type": fieldType,
      "code": fieldCode,
      "label": e[COLUMNS.fieldLabel],
      "required": (e[COLUMNS.isRequired] === "" || e[COLUMNS.isRequired] === "false") ? false : true,
      "unique": e[COLUMNS.isUnique] === "" || e[COLUMNS.isUnique] === "false" ? false : true,
      "noLabel": e[COLUMNS.noLabel] === "" || e[COLUMNS.noLabel] === "false" ? false : true,
    };
    // 選択肢が必要なフィールドかチェック
    const needChoices = definitions.checkNeedOpitons(FIELD_TYPES[e[COLUMNS.fieldType]]);
    if (needChoices && e[COLUMNS.choices] !== "") {
      // 入力された選択肢をカンマ区切りで分割
      const options = e[COLUMNS.choices].split(",");
      const optionsObj = {};
      for (let i = 0; i < options.length; i++) {
        optionsObj[options[i]] = {
          "label": options[i],
          "index": i
        }
      }
      // リクエストボディに選択肢を追加
      requestBody.options = optionsObj;
    }

    // リンクフィールドの場合リンクのタイプをリクエストボディに追加
    if (fieldType === FIELD_TYPES.リンク) requestBody.protocol = e[COLUMNS.linkType];
    // 計算フィールドの場合計算式をリクエストボディに追加
    if (fieldType === FIELD_TYPES.計算) requestBody.expression = e[COLUMNS.formula];

    // 行番号がサブテーブルの行番号と同じか判定
    const subTblIndex = subTblRowNo.find(x => x === String(e[COLUMNS.row]));
    if (!subTblIndex) {
      fieldRegistrationBody.properties[fieldCode] = requestBody;
    } else {
      // サブテーブルの中にフィールドの場合サブテーブル配下にリクエストボディを追加
      fieldRegistrationBody.properties[subtableRowNoObj[subTblIndex]].fields[fieldCode] = requestBody;
    }

    // レイアウトのリクエスボディにフィールド情報を追加
    layoutBody.layout[e[COLUMNS.row] - 1].fields.push({
      "type": fieldType,
      "code": fieldCode
    });
  });

  return [fieldRegistrationBody, layoutBody];
}

選択肢が必要なフィールドタイプかチェックする関数を Definitions.gs に追加

this.checkNeedOpitons = (fieldType) => {
   const needChoices = [
    this.FIELD_TYPES.チェックボックス,
    this.FIELD_TYPES.複数選択,
    this.FIELD_TYPES.ドロップダウン,
    this.FIELD_TYPES.ラジオボタン,
  ];
  return needChoices.includes(fieldType);
}

コード全体像

ファイル構成
・index.html
・main.gs
・Definitions.gs

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <form id="form" onsubmit="formSubmit(this)">
      <p>
      	<label for="domain">
          ◆サブドメイン ※https://〇〇.cybozu.com の〇〇だけ入力してください。<br>
        </label>
	      <input type="text" name="domain" id="domain" required>
      </p>
      <p>
      	<label for="token">
          ◆APIトークン ※アプリ管理権限のAPIトークンが必要です<br>
        </label>
	      <input type="text" name="token" id="token" style="width: 400px" required>
      </p>
      <p>
      	<label for="app-id">
          ◆アプリID<br>
        </label>
	      <input type="number" name="appId" id="appId" style="width: 50px" required>
      </p>
      <input type="submit" value="登録" />
    </form>
    <!-- 実行中... を表示させるためのタグ -->
    <div id="info"></div>
    <script>
      const formSubmit = (formObj) => {
        const el = document.getElementById("info").appendChild(document.createElement("p"));
        el.textContent = "実行中...";
        google.script.run.withSuccessHandler(() => {
          google.script.host.close();
        }).withFailureHandler((err) => {
          alert(err);
        }).main(formObj);
      }
    </script>
  </body>
</html>
class Definitions {
  constructor () {
    this.COLUMNS = {
      "row": 0,
      "fieldLabel": 1,
      "fieldCode": 2,
      "fieldType": 3,
      "noLabel": 4,
      "isRequired": 5,
      "isUnique": 6,
      "choices": 7,
      "linkType": 8,
      "formula": 9
    }

    this.FIELD_TYPES = {
      "文字列1行": "SINGLE_LINE_TEXT",
      "文字列複数行": "MULTI_LINE_TEXT",
      "リッチエディタ": "RICH_TEXT",
      "リンク": "LINK",
      "数値": "NUMBER",
      "計算": "CALC",
      "チェックボックス": "CHECK_BOX",
      "ドロップダウン": "DROP_DOWN",
      "ラジオボタン": "RADIO_BUTTON",
      "複数選択": "MULTI_SELECT",
      "日付": "DATE",
      "時刻": "TIME",
      "日時": "DATETIME",
      "添付ファイル": "FILE",
      "ユーザー選択": "USER_SELECT",
      "グループ選択": "GROUP_SELECT",
      "組織選択": "ORGANIZATION_SELECT",
      "サブテーブル": "SUBTABLE",
      "レコード番号": "RECORD_NUMBER",
      "作成者": "CREATOR",
      "更新者": "MODIFIER",
      "作成日時": "CREATED_TIME",
      "更新日時": "UPDATED_TIME"
    }

    this.checkNeedOpitons = (fieldType) => {
      const needChoices = [
        this.FIELD_TYPES.チェックボックス,
        this.FIELD_TYPES.複数選択,
        this.FIELD_TYPES.ドロップダウン,
        this.FIELD_TYPES.ラジオボタン,
      ];
      return needChoices.includes(fieldType);
    }
  }
}
/**
 * @param {Object} formObj
 * @param {string} formObj.domain
 * @param {string} formObj.token
 * @param {number} formObj.appId
 */
const main = (data) => {
  try{
    const definitions = new Definitions();
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
    const lastRow = sheet.getLastRow();
    // スプレッドシートに入力された情報を全て取得
    const values = sheet.getRange(2, 1, lastRow - 1, Object.keys(definitions.COLUMNS).length).getValues();
    // フォームに入力された情報
    const apiToken = data.token;
    const appId = data.appId;
    const domain = data.domain;

    // リクエストボディを作成する getRequestBody 関数に必要な情報を渡す。
    const [fieldRegistrationBody, layoutBody] = getRequestBody(appId, values, definitions);
    const options = {
      "method": "post",
      "contentType": "application/json",
      "muteHttpExceptions" : true,
      "headers": {
        "X-Cybozu-API-Token": apiToken
      },
      "payload": JSON.stringify(fieldRegistrationBody)
    };
    // フィールド登録APIリクエストを送る
    const fetchFieldsUrl = `https://${domain}.cybozu.com/k/v1/preview/app/form/fields.json`;
    const resp1 = UrlFetchApp.fetch(fetchFieldsUrl,options);
    // リクエストが失敗したらエラーメッセージを投げる
    if (resp1.getResponseCode() >= 400) throw new Error(resp1.getContentText());
    
    // アプリに登録されたフィールドの位置を変更するリクエストなのでputで送る
    options.method = "put";
    options.payload = JSON.stringify(layoutBody);
    // フィールドレイアウト更新APIにリクエストを送る
    const fetchLayoutUrl = `https://${domain}.cybozu.com/k/v1/preview/app/form/layout.json`;
    const resp2 = UrlFetchApp.fetch(fetchLayoutUrl,options);
    // リクエストが失敗したらエラーメッセージを投げる
    if (resp2.getResponseCode() >= 400) throw new Error(resp2.getContentText());
    
    // リクエストが成功したらmsgBoxにsuccessMsgを表示して終了
    const successMsg = `処理が完了しました。設定画面で結果を確認して下さい。https://${domain}.cybozu.com/k/admin/app/flow?app=${appId}#section=form`;
    Browser.msgBox(successMsg);
  } catch (err) {
    Browser.msgBox(err)
    console.error(err);
  }
}

const getRequestBody = (appId, values, definitions) => {
  const COLUMNS = definitions.COLUMNS;
  const FIELD_TYPES = definitions.FIELD_TYPES;
  // フィールド登録用のリクエストボディ
  const fieldRegistrationBody = {
    "app": appId,
    "properties": {}
  }
  // フィールドレイアウト更新用のリクエストボディ
  const layoutBody = {
    "app": appId,
    "layout": []
  }

  // 行番号(A列)の値で昇順にソート
  values.sort((a, b) => {
    return a[0] - b[0];
  });

  // サブテーブルの行番号とフィールドコードを代入する変数
  const subtableRowNoObj = {};
  // サブテーブル以外の行番号を代入する変数
  const normalFieldRowNo = [];
  // サブテーブル以外のフィールド情報を代入する変数
  const normalFieldValues = [];
  
  values.forEach(e => {
    // SSに入力されたフィールドコード(C列)を代入
    const fieldCode = e[COLUMNS.fieldCode];
    // SSに入力されたフィールドタイプ(D列)の情報を英語に変換して代入
    const fieldType = FIELD_TYPES[e[COLUMNS.fieldType]];

    if(fieldType === FIELD_TYPES.サブテーブル) {
      // サブテーブルの行番号が重複していないかチェック
      if(Object.keys(subtableRowNoObj).includes(String(e[COLUMNS.row]))) {
        throw new Error(`フィールドコード:${fieldCode}の行番号が重複しています。サブテーブルは行番号の重複ができません。`);
      }
      // フィールド登録用リクエストボディにサブテーブルの情報を追加
      fieldRegistrationBody.properties[fieldCode] = {
        "type": fieldType,
        "code": fieldCode,
        "label": e[COLUMNS.fieldLabel],
        "noLabel": e[COLUMNS.noLabel] === "" || e[COLUMNS.noLabel] === "false" ? false : true,
        "fields": {} // fieldsにはサブテーブル内のフィールド情報を追加していく
      }
      // サブテーブルの行番号とフィールドコードを代入
      subtableRowNoObj[String(e[COLUMNS.row])] = fieldCode;
      // レイアウト更新用リクエストボディにサブテーブルの情報を追加
      layoutBody.layout[e[COLUMNS.row] - 1] = {
        "type": FIELD_TYPES.サブテーブル,
        "code": fieldCode,
        "fields": [] // fieldsにはサブテーブル内のフィールド情報を追加していく
      }
    } else {
      // サブテーブル以外のフィールドの処理

      // classに定義した選択肢が必要か判定する関数でチェック。booleanで返却
      const needChoices = definitions.checkNeedOpitons(fieldType);
      // 選択肢が必要なフィールドでSSに入力がなければエラーを返す
      if (needChoices && e[COLUMNS.choices] === "") throw new Error(`フィールドコード:${fieldCode}は選択肢の入力が必要です`);
      // リンクフィールドでリンクタイプの指定がなければエラーを返す
      if (fieldType === FIELD_TYPES.リンク && e[COLUMNS.linkType] === "") throw new Error(`フィールドコード:${fieldCode}ははリンクタイプの指定が必要です。`);
      if (fieldType === FIELD_TYPES.計算 && e[COLUMNS.formula] === "") throw new Error(`フィールドコード:${fieldCode}は計算式が必要です。`);
      // サブテーブル以外のフィールドの情報を配列に追加
      normalFieldValues.push(e);
      // サブテーブル以外のフィールドの行番号を配列に追加。レイアウト用のリクエストボディ作成に利用
      normalFieldRowNo.push(String(e[COLUMNS.row]));
    }
  });

  // サブテーブル以外フィールドの行番号の重複を排除
  const uniqueNormalFieldRowNo = Array.from(new Set(normalFieldRowNo));
  // サブテーブルの行番号を配列で代入
  const subTblRowNo = Object.keys(subtableRowNoObj);
  // uniqueNormalFieldRowNo からサブテーブルと同じ行番号を排除する
  const rowNoWithoutSubTbl = uniqueNormalFieldRowNo.filter(i => subTblRowNo.indexOf(i) === -1);
  // サブテーブルに含まれないフィールドのレイアウト用リクエストボディ
  rowNoWithoutSubTbl.forEach(e => {
    layoutBody.layout[e - 1] = {
      "type": "ROW",
      "fields": []
    }
  });

  // 通常フィールドのリクエストボディを作成
  normalFieldValues.forEach(e => {
    const fieldCode = e[COLUMNS.fieldCode];
    const fieldType = FIELD_TYPES[e[COLUMNS.fieldType]];
    const requestBody = {
      "type": fieldType,
      "code": fieldCode,
      "label": e[COLUMNS.fieldLabel],
      "required": (e[COLUMNS.isRequired] === "" || e[COLUMNS.isRequired] === "false") ? false : true,
      "unique": e[COLUMNS.isUnique] === "" || e[COLUMNS.isUnique] === "false" ? false : true,
      "noLabel": e[COLUMNS.noLabel] === "" || e[COLUMNS.noLabel] === "false" ? false : true,
    };
    // 選択肢が必要なフィールドかチェック
    const needChoices = definitions.checkNeedOpitons(FIELD_TYPES[e[COLUMNS.fieldType]]);
    if (needChoices && e[COLUMNS.choices] !== "") {
      // 入力された選択肢をカンマ区切りで分割
      const options = e[COLUMNS.choices].split(",");
      const optionsObj = {};
      for (let i = 0; i < options.length; i++) {
        optionsObj[options[i]] = {
          "label": options[i],
          "index": i
        }
      }
      // リクエストボディに選択肢を追加
      requestBody.options = optionsObj;
    }

    // リンクフィールドの場合リンクのタイプをリクエストボディに追加
    if (fieldType === FIELD_TYPES.リンク) requestBody.protocol = e[COLUMNS.linkType];
    // 計算フィールドの場合計算式をリクエストボディに追加
    if (fieldType === FIELD_TYPES.計算) requestBody.expression = e[COLUMNS.formula];

    // 行番号がサブテーブルの行番号と同じか判定
    const subTblIndex = subTblRowNo.find(x => x === String(e[COLUMNS.row]));
    if (!subTblIndex) {
      fieldRegistrationBody.properties[fieldCode] = requestBody;
    } else {
      // サブテーブルの中にフィールドの場合サブテーブル配下にリクエストボディを追加
      fieldRegistrationBody.properties[subtableRowNoObj[subTblIndex]].fields[fieldCode] = requestBody;
    }

    // レイアウトのリクエスボディにフィールド情報を追加
    layoutBody.layout[e[COLUMNS.row] - 1].fields.push({
      "type": fieldType,
      "code": fieldCode
    });
  });

  return [fieldRegistrationBody, layoutBody];
}

動かしてみる

このデータが

ちゃんと反映できた。

まとめ

リクエストボディ作る関数長すぎたな...

あと作ってて思ったけどAPIトークンの認証じゃなくてパスワード認証のほうが良かったかもなー、気が向いたら修正する。