GASを使ってBacklogの課題を取得する方法


## やりたいこと
プロジェクト管理ツールであるBacklogの課題には、状況や時間など作業の情報がたくさん詰まっています。
そんな課題の情報を利用して報告書などの資料を作ることがあります。
そのために、資料の基となる課題の情報をBacklogAPIを使って取得します。
取得した課題の情報をそのまま加工してGoogleスプレッドシートで資料を作成出来るようにGAS(Google Apps Script)を使います。
![2023-05-22 11.57.50.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1743295/e87da616-442e-3163-46bc-c1302b509592.jpeg)

この記事では、課題情報を取得する方法を記載します。Googleスプレッドシートで課題の情報を加工成型する処理は別記事でご紹介いたします。

:::note warn
ご利用は自己責任でお願いいたします。
:::

## 1. BacklogAPIに必要な情報を取得する
BacklogAPIを使うためには「BacklogのURL」「APIキー」が必要になります。
参考 : [APIの設定 &ndash; Backlog ヘルプセンター](https://support-ja.backlog.com/hc/ja/articles/360035641754-API%E3%81%AE%E8%A8%AD%E5%AE%9A)

課題の取得では「プロジェクトキー」「マイルストーン」を条件に使用します。


### 1-1. スプレッドシートから設定情報を取得する
「BacklogのURL」「プロジェクトキー」「マイルストーン」は、スプレッドシートに記載してある内容を取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1743295/b9ba56b1-d4c7-7ab8-4b15-b8970b0d034d.png)

特定シート名のシートから取得して、オブジェクトとして保持します。

```js
/**
 * [input]シートの情報を取得する
 * @return {object} info 設定情報.
 */
function getInputInfo() {
  const workFile = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = workFile.getSheetByName('input');
  if(!sheet) {
    // [input]シートがない場合
    return null;
  }

  let url = sheet.getRange('B2').getValue();
  if(url && url.endsWith('/')) {
    // URLが「/」で終わっている場合に「/」を削除する(エンドポイントを設定する場合に「/」を付加するため)
    url = url.substring(0, url.length-1);
  }
  const projectKey = sheet.getRange('B3').getValue();
  const milestone = sheet.getRange('B4').getValue();
  if(!url || !projectKey || !milestone){
    // [input]シートに設定項目値が記載されていない場合
    return null;
  }

  return {
    'url' : url,
    'apiKey' : getApiKey(),
    'projectKey' : projectKey,
    'milestone' : milestone,
  }
}
```



### 1-2. APIキー登録機能作成
APIキーだけは、GoogleのPropertiesServiceから取得します。

> *Properties Service*
> このサービスでは、1 つのスクリプト、1 つのスクリプトのユーザー、またはエディタ アドオンが使用されているドキュメントにスコープが設定された Key-Value ペアとして、文字列を格納できます。
> [Properties Service &nbsp;|&nbsp; Apps Script &nbsp;|&nbsp; Google for Developers](https://developers.google.com/apps-script/reference/properties?hl=ja)

「APIキー」は、認証情報の一種であり、秘密情報として扱う必要があります。
そのため、スプレッドシートには記載せずPropertiesServiceに保存して他の人には見えないように扱います。

登録機能を作ります。「ボタンを押下するとダイアログが表示されて、値を入力して登録する」という機能です。
![hoge.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1743295/d4f80f37-1c74-a44b-3dab-d0755fee7005.gif)

Properties Serviceの「Key-Value ペア」の「Key」となる文字列は定数として最初に定義しておきます。

```js
/** @OnlyCurrentDoc */
const apiKeyPropertyKeyName = 'apiKey';
```

登録機能で注意するポイントはPropertiesServiceは、UserPropertiesを使用するというところです。

> **getUserProperties()**
> 現在のユーザーのみがこのスクリプト内でのみアクセスできるプロパティ ストアを取得します。
> [Class PropertiesService &nbsp;|&nbsp; Apps Script &nbsp;|&nbsp; Google Developers](https://developers.google.com/apps-script/reference/properties/properties-service?hl=ja#getuserproperties)

```js
/**
 * BacklogのapiKeyをPropertiesServiceに設定する.
 * @patam {string} BacklogのapiKey.
 */
function setApiKey() {
  const ui = SpreadsheetApp.getUi();
  // ダイアログからAPIキーを入力させる
  const response = ui.prompt('BacklogのAPIキーを入力してください。', ui.ButtonSet.OK_CANCEL);
  if (response.getSelectedButton() == ui.Button.OK) {
    // [OK]ボタンが押下された場合に入力値を取得する
    const apiKey = response.getResponseText();
    if (apiKey) {
      // 値が入力された場合にPropertiesServiceへ保存する
      PropertiesService.getUserProperties().setProperty(apiKeyPropertyKeyName, apiKey);
      Browser.msgBox('BacklogのAPIキーを「' + apiKey + '」で保存しました。');
    }
  }
}
```

誤登録や変更のために削除機能も作ります。

```js
/** apiKeyをPropertiesServiceから削除する. */
function deleteApiKey() {
  PropertiesService.getUserProperties().deleteProperty(apiKeyPropertyKeyName);
}
```





## 2. BacklogAPIから課題を取得する
課題を取得する条件は「プロジェクト」「マイルストーン」です。
しかし、BacklogAPIでは「プロジェクトのID」「マイルストーンのID」を指定する必要があります。
参考 : [課題一覧の取得 | Backlog Developer API | Nulab](https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list/#)

「プロジェクトキー」「マイルストーン名」は、URLや画面で確認しやすいですが、
「プロジェクトのID」「マイルストーンのID」はなかなか確認しにくいです。
そのため、「プロジェクトキー」「マイルストーン名」をスプレッドシートに記載して、その値から各IDを取得していきます。

### 2-1. プロジェクトIDを取得する
[Backlog APIの「プロジェクト一覧の取得」](https://developer.nulab.com/ja/docs/backlog/api/2/get-project-list/#)からプロジェクト一覧を取得して「プロジェクトキー」に一致するプロジェクトのIDを返却するメソッドにします。

```js
/**
 * BacklogのプロジェクトIDを取得する.
 * @param {object} info 設定情報.
 * @return {number} プロジェクトID.
 */
function getProjectId(info) {
  /** @type {number} プロジェクトID. */
  let id = null;
  /** @type {string} BacklogAPIのエンドポイント(https://developer.nulab.com/ja/docs/backlog/api/2/get-project-list/#). */
  const endpoint = `${info.url}/api/v2/projects?apiKey=${info.apiKey}`;
  /** @type {HTTPResponse} APIの返却データ. */
  const httpResponse = UrlFetchApp.fetch(endpoint);

  let projects = JSON.parse(httpResponse.getContentText());
  const project = projects.find(project => project.projectKey == info.projectKey);
  if(project) {
    id = project.id;
  }
  return id;
}
```


### 2-2. マイルストーンIDを取得する
[Backlog APIの「バージョン(マイルストーン)一覧の取得」](https://developer.nulab.com/ja/docs/backlog/api/2/get-version-milestone-list/#)からマイルストーン一覧を取得して「マイルストーン名」に一致するマイルストーンのIDを返却するメソッドにします。

```js
/**
 * BacklogのマイルストーンIDを取得する.
 * @param {object} info 設定情報.
 * @return {number} マイルストーンID.
 */
function getMilestoneId(info) {
  /** @type {number} マイルストーンID. */
  let milestoneId = null;
  /** @type {string} BacklogAPIのエンドポイント(https://developer.nulab.com/ja/docs/backlog/api/2/get-version-milestone-list/#). */
  const endpoint = `${info.url}/api/v2/projects/${info.projectKey}/versions?apiKey=${info.apiKey}`;
  /** @type {HTTPResponse} APIの返却データ. */
  const httpResponse = UrlFetchApp.fetch(endpoint);

  let milestones = JSON.parse(httpResponse.getContentText());
  const milestone = milestones.find(milestone => milestone.name == info.milestone);
  if(milestone) {
    milestoneId = milestone.id;
  }
  return milestoneId;
}
```


### 2-3. 課題を取得する

[BacklogAPIの「課題一覧の取得」](https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list/#)で「プロジェクトのID」「マイルストーンのID」を条件に課題を取得します。
取得した課題は、後続の成型処理(別記事でご紹介します)で使いやすいようにJSON形式に変換しておきます。

```js
/**
 * Backlogの課題を取得する.
 * @param {object} info 設定情報.
 * @param {number} projectId プロジェクトID.
 * @param {number} milestoneId マイルストーンID.
 * @return {Array<string>} 課題のJSONを格納した配列.
 */
function getIssues(info, projectId, milestoneId) {
  /** @type {Array<string>} 課題のJSONを格納した配列. */
  let issues = [];
  /** @type {string} BacklogAPIのエンドポイント(https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list/#). */
  let endpoint = [`${info.url}/api/v2/issues?apiKey=${info.apiKey}`];
  endpoint.push(`projectId[]=${projectId}`);
  endpoint.push(`milestoneId[]=${milestoneId}`);
  /** @type {HTTPResponse} APIの返却データ. */
  let httpResponse = UrlFetchApp.fetch(endpoint.join('&'));

  issues = JSON.parse(httpResponse.getContentText());
  return issues;
}
```








# コード全体

<details><summary>最後にコード全体を記載します。長いので気になった方のみご参照ください。「▶」をクリックすると表示します。</summary><div>

```js
/** @OnlyCurrentDoc */
const apiKeyPropertyKeyName = 'apiKey';

/**
 * Backlogより課題を取得する.
 * @return {Array<string>} 課題のJSONを格納する配列.
 * @customfunction
 */
function getBacklogInfo() {
  /** @type {Array<string>} 課題のJSONを格納する配列. */
  let issues = null;
  const info = getInputInfo();
  if(!info) {
    Browser.msgBox('設定情報が取得できないため実行できません');
    return null;
  }
  try {
    const projectId = getProjectId(info);
    if(!projectId) {
      Browser.msgBox('指定された「プロジェクトキー」が存在しません。');
      return null;
    }
    const milestoneId = getMilestoneId(info);
    if(!milestoneId) {
      Browser.msgBox('指定された「マイルストーン」が存在しません。');
      return null;
    }
    issues = getIssues(info, projectId, milestoneId);
  } catch(e) {
    // [muteHttpExceptions]=「false(デフォルト)」にしているためAPI処理のHTTPエラー(4xxまたは5xx)はここでまとめて処理する
    Browser.msgBox('Backlog課題取得に失敗しました。\\n' + e.message);
  }
  return issues;
}

/**
 * [input]シートの情報を取得する
 * @return {object} info 設定情報.
 */
function getInputInfo() {
  const workFile = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = workFile.getSheetByName('input');
  if(!sheet) {
    // [input]シートがない場合
    return null;
  }

  let url = sheet.getRange('B2').getValue();
  if(url && url.endsWith('/')) {
    // URLが「/」で終わっている場合に「/」を削除する(エンドポイントを設定する場合に「/」を付加するため)
    url = url.substring(0, url.length-1);
  }
  const projectKey = sheet.getRange('B3').getValue();
  const milestone = sheet.getRange('B4').getValue();
  if(!url || !projectKey || !milestone){
    // [input]シートに設定項目値が記載されていない場合
    return null;
  }

  return {
    'url' : url,
    'apiKey' : getApiKey(),
    'projectKey' : projectKey,
    'milestone' : milestone,
  }
}

/**
 * apiKeyを取得する.
 * @return {string} BacklogのapiKey.
 */
function getApiKey() {
  return PropertiesService.getUserProperties().getProperty(apiKeyPropertyKeyName);
}

/**
 * BacklogのapiKeyをPropertiesServiceに設定する.
 * @patam {string} BacklogのapiKey.
 */
function setApiKey() {
  const ui = SpreadsheetApp.getUi();
  // ダイアログからAPIキーを入力させる
  const response = ui.prompt('BacklogのAPIキーを入力してください。', ui.ButtonSet.OK_CANCEL);
  if (response.getSelectedButton() == ui.Button.OK) {
    // [OK]ボタンが押下された場合に入力値を取得する
    const apiKey = response.getResponseText();
    if (apiKey) {
      // 値が入力された場合にPropertiesServiceへ保存する
      PropertiesService.getUserProperties().setProperty(apiKeyPropertyKeyName, apiKey);
      Browser.msgBox('BacklogのAPIキーを「' + apiKey + '」で保存しました。');
    }
  }
}

/** apiKeyをPropertiesServiceから削除する. */
function deleteApiKey() {
  PropertiesService.getUserProperties().deleteProperty(apiKeyPropertyKeyName);
}

/**
 * BacklogのプロジェクトIDを取得する.
 * @param {object} info 設定情報.
 * @return {number} プロジェクトID.
 */
function getProjectId(info) {
  /** @type {number} プロジェクトID. */
  let id = null;
  /** @type {string} BacklogAPIのエンドポイント(https://developer.nulab.com/ja/docs/backlog/api/2/get-project-list/#). */
  const endpoint = `${info.url}/api/v2/projects?apiKey=${info.apiKey}`;
  /** @type {HTTPResponse} APIの返却データ. */
  const httpResponse = UrlFetchApp.fetch(endpoint);

  let projects = JSON.parse(httpResponse.getContentText());
  const project = projects.find(project => project.projectKey == info.projectKey);
  if(project) {
    id = project.id;
  }
  return id;
}

/**
 * BacklogのマイルストーンIDを取得する.
 * @param {object} info 設定情報.
 * @return {number} マイルストーンID.
 */
function getMilestoneId(info) {
  /** @type {number} マイルストーンID. */
  let milestoneId = null;
  /** @type {string} BacklogAPIのエンドポイント(https://developer.nulab.com/ja/docs/backlog/api/2/get-version-milestone-list/#). */
  const endpoint = `${info.url}/api/v2/projects/${info.projectKey}/versions?apiKey=${info.apiKey}`;
  /** @type {HTTPResponse} APIの返却データ. */
  const httpResponse = UrlFetchApp.fetch(endpoint);

  let milestones = JSON.parse(httpResponse.getContentText());
  const milestone = milestones.find(milestone => milestone.name == info.milestone);
  if(milestone) {
    milestoneId = milestone.id;
  }
  return milestoneId;
}

/**
 * Backlogの課題を取得する.
 * @param {object} info 設定情報.
 * @param {number} projectId プロジェクトID.
 * @param {number} milestoneId マイルストーンID.
 * @return {Array<string>} 課題のJSONを格納した配列.
 */
function getIssues(info, projectId, milestoneId) {
  /** @type {Array<string>} 課題のJSONを格納した配列. */
  let issues = [];
  /** @type {string} BacklogAPIのエンドポイント(https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list/#). */
  let endpoint = [`${info.url}/api/v2/issues?apiKey=${info.apiKey}`];
  endpoint.push(`projectId[]=${projectId}`);
  endpoint.push(`milestoneId[]=${milestoneId}`);
  /** @type {HTTPResponse} APIの返却データ. */
  let httpResponse = UrlFetchApp.fetch(endpoint.join('&'));

  issues = JSON.parse(httpResponse.getContentText());
  Logger.log(issues);
  return issues;
}

```
</div></details>