HRMOS勤怠by IEYASUにAPI連携するGAS作っていく #2

非エンジニアのバックオフィスがAPI連携を頑張るシリーズです。
いよいよ[HARMOS勤怠](https://hrmos.co/kintai/)にAPI連携していきます
# この記事のゴール：ユーザー一覧を取得してスプレッドシートに転記する
月次レポートを取得する前に、まずはユーザー一覧をAPIで取得して、スプレッドシートに転記します。月次レポートのユーザーを特定するために、ユーザーIDと社員ID、氏名などが紐づいたデータが必要だからです。
ここではChatGPTを使っていきます。
```:プロンプト
月次レポートを取得する前に、ユーザー一覧を取得します。ユーザー一覧の仕様は下記です。
https://ieyasu.co/docs/api.html#/paths/~1users/get

下記に改めて記載するgetToken関数のコードをそのまま使用し、取得したトークンを使用し、ユーザー一覧を取得します。
レスポンスの「クエリ対象の全件数」を取得できるよう、パラメータの取得件数最大値と対象ページをコントロールしながら全件もれなく取得してください。
UrlFetchAppクラスで取得ましょう。
取得できたら、Googleスプレッドシートに転記します。
シート名"ユーザー一覧"に1レコード1件として転記していきます。
すでにシートが存在する場合には、レコードをすべて削除して上書きするようにしてください。
変数の宣言は varではなく、なるべくconstを使用してください。今後書くコードもすべてそこを意識してください。

function getToken() {
  const ps = PropertiesService.getScriptProperties()
  const apiKey = ps.getProperty('HRMOS_API_KEY')
  const endPoint = 'https://ieyasu.co/api/' + ps.getProperty('HRMOS_COMPANY_URL') + '/v1/authentication/token'
  const headerInfo = token => ({
    'Content-Type': 'application/json',
    'Authorization': 'Basic ' + token
  })
  const options = {
    method: 'get',
    headers: headerInfo(apiKey),
    "muteHttpExceptions": true,
  }
  const response = UrlFetchApp.fetch(endPoint, options)
  const json = JSON.parse(response.getContentText())
  return json.token
}
```
プロンプトを作成する際の注意点ですが、明示して指示をしないと、せっかくうまく機能している getToken関数をわざわざ改変しちゃったりするので、ここを固定化する指示出しをします。
加えてなにも言わないとvarで宣言することが多いので、あえてconstを使うように指示しておきます。僕はコードは書けませんけど、先人の記事などを見るとconstを使用すべきかなと判断しました。

結果として以下のコードが出力されました。
```javascript:出力結果
function getToken() {
  const ps = PropertiesService.getScriptProperties();
  const apiKey = ps.getProperty('HRMOS_API_KEY');
  const endPoint = 'https://ieyasu.co/api/' + ps.getProperty('HRMOS_COMPANY_URL') + '/v1/authentication/token';
  const headerInfo = token => ({
    'Content-Type': 'application/json',
    'Authorization': 'Basic ' + token
  });
  const options = {
    method: 'get',
    headers: headerInfo(apiKey),
    "muteHttpExceptions": true
  };
  const response = UrlFetchApp.fetch(endPoint, options);
  const json = JSON.parse(response.getContentText());
  return json.token;
}

function fetchAndSaveUsers() {
  const token = getToken();
  const ps = PropertiesService.getScriptProperties();
  const companyUrl = ps.getProperty('HRMOS_COMPANY_URL');
  const url = `https://ieyasu.co/api/${companyUrl}/v1/users`;
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + token
  };
  
  const options = {
    method: 'get',
    headers: headers
  };
  
  let users = [];
  let page = 1;
  let totalCount = 0;

  do {
    const response = UrlFetchApp.fetch(`${url}?per_page=100&page=${page}`, options);
    const responseData = JSON.parse(response.getContentText());
    totalCount = responseData.total_count;
    users = users.concat(responseData.users);
    page++;
  } while (users.length < totalCount);

  const sheetName = 'ユーザー一覧';
  let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  if (!sheet) {
    sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(sheetName);
  } else {
    sheet.clear();
  }

  if (users.length > 0) {
    const headers = Object.keys(users[0]);
    sheet.appendRow(headers);

    users.forEach(user => {
      const row = headers.map(header => user[header]);
      sheet.appendRow(row);
    });
  }

  Logger.log('Users data saved successfully.');
}
```
# 怒涛のデバッグ作業
ChatGPTの書くコードは結構いい加減なようで、上記のコードはエラーとなります。（だいたいのコードは一発で動くことはありません）
私はコードはほぼ読めませんので、実行ログのエラーメッセージをプロンプトとして投入してChatGPT自身にデバッグしてもらいます。意図しない結果についても状況をログと合わせてプロンプトから教えてあげるようにします。するとChatGPTのほうからデバッグのためにコンソールに出力する仕掛けなどを提示して盛り込んできますので、そこで数回やりとりをすれば、だいたいデバッグが完了するイメージです。

今回、2回ほどエラーメッセージを投入して修正したところ、期待通りにスプレッドシートに転記することに成功していました。ただし1度に最大100件まで取得できるのに。25件ずつ取得しているように見えましたので、クエリパラメータで最大件数が設定できることと、レスポンスヘッダでクエリ対象の全件数と最大ページ数が取得できることから、それらを利用してコードを最適化するよう指示しました。

結果、エラーやデグレードなどで6回の指示を繰り返し、あれやこれや30分ほどかけて完成したのが次のコードです。動作に問題ないことを確認のうえ、デバッグに使用したコンソールログ出力のコードを削除するように指示し、また適切な概要コメントを入れてもらうようにしました。
```javascript:ユーザー一覧取得・転記
/**
 * トークンを取得する関数
 * スクリプトプロパティからAPIキーと会社URLを取得し、トークンを取得します。
 * 
 * @return {string} APIトークン
 */
function getToken() {
  const ps = PropertiesService.getScriptProperties();
  const apiKey = ps.getProperty('HRMOS_API_KEY');
  const endPoint = 'https://ieyasu.co/api/' + ps.getProperty('HRMOS_COMPANY_URL') + '/v1/authentication/token';
  const headerInfo = token => ({
    'Content-Type': 'application/json',
    'Authorization': 'Basic ' + token
  });
  const options = {
    method: 'get',
    headers: headerInfo(apiKey),
    "muteHttpExceptions": true
  };
  const response = UrlFetchApp.fetch(endPoint, options);
  const json = JSON.parse(response.getContentText());
  return json.token;
}

/**
 * ユーザー一覧を取得してスプレッドシートに保存する関数
 * 取得したトークンを使用して、ユーザー一覧をAPIから取得し、
 * Googleスプレッドシートに転記します。
 */
function fetchAndSaveUsers() {
  const token = getToken(); // トークンを取得
  const ps = PropertiesService.getScriptProperties();
  const companyUrl = ps.getProperty('HRMOS_COMPANY_URL');
  const url = `https://ieyasu.co/api/${companyUrl}/v1/users`;
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + token
  };

  const options = {
    method: 'get',
    headers: headers,
    'muteHttpExceptions': true
  };

  let users = [];
  let page = 1;
  let totalPages = 1; // 初期値
  const limit = 100; // 1ページあたりの取得件数

  // ページネーションを使用して全ユーザーを取得
  do {
    const response = UrlFetchApp.fetch(`${url}?limit=${limit}&page=${page}`, options);
    const responseData = JSON.parse(response.getContentText());
    const responseHeaders = response.getAllHeaders();

    // レスポンスヘッダーから全件数と全ページ数を取得
    if (responseHeaders['x-total-count'] && responseHeaders['x-total-page']) {
      const totalCount = parseInt(responseHeaders['x-total-count'], 10);
      totalPages = parseInt(responseHeaders['x-total-page'], 10);
    }

    // レスポンスデータが配列であることを確認
    if (Array.isArray(responseData)) {
      users = users.concat(responseData); // ユーザーデータを追加
      page++;
    } else {
      break;
    }
  } while (page <= totalPages); // 全ページを処理するまでループ

  const sheetName = 'ユーザー一覧';
  let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  if (!sheet) {
    sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(sheetName); // シートが存在しない場合は新規作成
  } else {
    sheet.clear(); // 既存のシートをクリア
  }

  // ユーザーデータをスプレッドシートに転記
  if (users.length > 0) {
    const headers = Object.keys(users[0]); // ユーザーデータのキーをヘッダーとして取得
    sheet.appendRow(headers);

    users.forEach((user, index) => {
      const row = headers.map(header => user[header]);
      sheet.appendRow(row);
    });
  }
}
```
# まとめ
一応これでAPI連携に成功してユーザー一覧の取得ができるようになりました。気づいた点をまとめます。
* ChatGPTのコードは一発で動かないことがほとんど
* エラーが出たり思い通りの結果にならなかった場合は、その状況を改めてプロンプトから伝えてあげたり、コンソールログをそのまま投入してデバッグしてもらう（エラーだと伝えずにコンソールログだけ投入してもちゃんと理解してデバッグしてくれる）
* そもそもAPI仕様書の理解度は薄め。こちらである程度フォローしてあげる必要あり

とはいえ、まったくコードを書く必要がないのですから夢が広がる世界ですね。
次回はいよいよ月次レポートを取得する処理をやっていきたいと思います。