ExcelのOfficeScriptsでBacklogの課題を取得してみよう

# はじめに
Microsoft 365やOneDirve Businessなどのプランを契約していて管理者が自動化を有効にしている場合、Excel on the webには作業を自動化する方法として下記の２つの方法が用意されています。

- PowerAutomateを利用する
- OfficeScriptsを利用する

この記事では、OfficeScriptsを使ってNulabの[Backlog](https://backlog.com/)から課題の情報を取得する方法について説明します。

# Backlogから課題を取得する

Backlogの情報を取得するにはBacklogが用意している[Backlog API](https://developer.nulab.com/ja/docs/backlog/)を利用して各種の情報を取得します。Excel on the webでは5/27に一般公開になった[OfficeScripts](https://docs.microsoft.com/ja-jp/office/dev/scripts/)を使用してExcelからこのAPIを呼び出すことができます。

JavaScriptからBacklog APIを呼び出す場合、Nulabが提供する[backlog-js](https://github.com/nulab/backlog-js)を利用すると簡単に機能を実装できますが、現時点のOfficeScriptsでは外部ライブラリが利用できないため自前でAPIを呼び出す必要があります。

# Backlog APIキーの準備
[Backlog APIの認可と認証](https://developer.nulab.com/ja/docs/backlog/auth/#)にはAPIキーを利用した方法と、OAuth 2.0を利用した方法が説明されていますがOffceScriptsではAPIキーを利用した方法で認証と認可を行います。
まずはBacklogヘルプセンターの[APIの設定](https://support-ja.backlog.com/hc/ja/articles/360035641754-API%E3%81%AE%E8%A8%AD%E5%AE%9A)に従って、API連携を行いたいBacklogユーザーのAPIキーを発行します。

# OfficeScriptsをはじめる
OfficeScriptsを新たに作成する場合は、Excelの`自動化]` > `すべてのスクリプト` > `+ 新しいスクリプト` からスクリプトを作成していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/8a921370-e5a2-8d46-0030-4aec788339c2.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/d1ccf57f-eb81-5afb-c9b7-25d587ab2e44.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/a4ba7ccd-944a-f6ce-e625-b8aef48de96c.png)

# BacklogAPIを呼び出してみる
OfficeScriptsから外部のAPIを呼び出す場合は[fetch API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API)を利用します。まずはBacklogの[スペース情報を取得](https://developer.nulab.com/ja/docs/backlog/api/2/get-space/#)してみましょう。
Backlog APIを呼び出すにはBacklogのURLとAPIキーが必要です。OfficeScriptsには秘密情報を管理する仕組みがないので設定値もシート上で管理してしまうのがお手軽です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/4dbc767d-aaed-1d9e-6668-b199974df724.png)
次の例では`Sheet1`シート上に定義されたBacklogURL（`B2`）とAPIキー（`B3`）の情報をもとにBacklog APIからスペース情報を取得し`B4`に表示しています。
async/awatを利用したいのでmain関数のシグネチャを`function main(workbook: ExcelScript.Workbook)`から`async function main(workbook: ExcelScript.Workbook): Promise<void>`に変更しています。また、OfficeScriptsでは型定義でAnyが許されない点に注意してください。

``` ts
async function main(workbook: ExcelScript.Workbook): Promise<void>
{
  const sheet = workbook.getWorksheet("Sheet1");
  const backlogUrl = sheet.getRange("B1").getValue();
  const apiKey = sheet.getRange("B2").getValue();

  const response = await fetch(`${backlogUrl}/api/v2/space?apiKey=${apiKey}`);
  const space: Space = await response.json();

  sheet.getRange("B4").setValue(space.spaceKey);
}

interface Space {
  spaceKey: string,
  name: string,
  ownerId: number
  lang: string,
  timezone: string,
  reportSendTime: string,
  textFormattingRule: string,
  created: string,
  updated: string
}
```
実行ボタンを押すとOfficeScriptsが実行されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/df0035e4-5a8c-a0eb-a3ee-111420c90234.png)

# Excelにテーブルを作成
課題一覧を取得する前に結果を表示するためのテーブルをExcel上に作成します。
7行目に取得したいデータの見出しを作成して、テーブルにしたい範囲を選択後にリボンの`ホーム` > `テーブルとして書式設定` からテーブルの書式を選択してテーブルにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/e5ba872a-e609-41c5-c364-f455f76c91b2.png)
`先頭行をテーブルの見出しとして使用する`をチェックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/5a732c75-13f3-9b05-c5f2-4dc11c1ec839.png)
このテーブルはわかりやすいように`課題一覧`という名前にしておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/b2a932d8-149f-dfb2-d6b5-f0133b574aff.png)

# 課題の取得
[課題一覧の取得API](https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list/#)を利用して課題を取得し課題の一覧を表示していきます。
先ほど設定したテーブルを取得後、いったん現在表示されている行をクリアします。

``` ts
  // テーブルの取得と初期化
  const dataTable = sheet.getTable("課題一覧");
  var rowCount = dataTable.getRowCount();
  if (rowCount > 0) {
    dataTable.deleteRowsAt(0, dataTable.getRowCount());
  }
```
その後、Backlogの[課題一覧の取得 API](https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list/#)を呼び出して先ほど初期化したテーブルの見出しに合う形に取得したデータを成型して追加します。Issueの型定義は長くなってしまうのでこの記事の最後に乗せた`付録 コード全体`を参照してください。

```ts
  // 課題の取得と表示
  const issues: Issue[] = await (await fetch(`${backlogUrl}/api/v2/issues?apiKey=${apiKey}`)).json();
  dataTable.addRows(-1, issues.map(i => 
  [ 
    i.issueKey, 
    i.summary, 
    i.status.name, 
    i.assignee.name
  ]));

```
どうですか？課題が課題一覧テーブルに表示されましたか？

# 外部API呼び出し時の注意点
OfficeScriptsの外部APIの呼び出しについて2つ注意点があります。
## API呼び出しはCORSの制約に縛られる
OfficeScriptsでfetch APIを利用してAPIを呼び出す場合、ブラウザーからJavaScriptを実行した場合と同様にCORSの制約に縛られます。
Backlog APIはどのオリジンからのリクエストも受け付けているため問題になりませんが、他のAPIを利用する場合はそのAPIがオリジンを超えてAPIを呼び出せるかを確認してください。CORS違反でAPIが呼び出せない場合ブラウザーの開発者ツールにエラーメッセージが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/1d3c5270-b00a-a3d5-53e0-8ac8dcf29f48.png)
## PowerAutomateからの実行
PowerAutomateからOfficeScriptsを呼び出す場合、呼び出し先のスクリプトにfetch APIがあると呼び出しに失敗します。[Office スクリプトでの外部 API 呼び出しのサポート - 外部からの外部通話Power Automate](https://docs.microsoft.com/ja-jp/office/dev/scripts/develop/external-calls#external-calls-from-power-automate)に記載がある通りセキュリティー上の制限です。「PowerAutomateを利用して定期的に外部APIを呼び出すOfficeScriptsを実行してデータを最新にする」というシナリオは現在のところ実現できません。
外部APIのデータを定期的に更新する場合はPowerAutomate側で処理を実行する必要があります。

# まとめ
- OfficeScriptsではfetch関数を使って外部APIを呼び出すことができる
- テーブルを使うと取得した一覧形式のデータを簡単に処理できる
- CORSやPowerAutomateからの実行などの制限に注意する

# 付録 コード全体

``` ts
async function main(workbook: ExcelScript.Workbook): Promise<void>
{
  const sheet = workbook.getWorksheet("Sheet1");
  const backlogUrl = sheet.getRange("B1").getValue();
  const apiKey = sheet.getRange("B2").getValue();

  const space: Space = await (await fetch(`${backlogUrl}/api/v2/space?apiKey=${apiKey}`)).json();

  sheet.getRange("B4").setValue(space.spaceKey);

  // テーブルの取得と初期化
  const dataTable = sheet.getTable("課題一覧");
  var rowCount = dataTable.getRowCount();
  if (rowCount > 0) {
    dataTable.deleteRowsAt(0, dataTable.getRowCount());
  }
  // 課題の取得と表示
  const issues: Issue[] = await (await fetch(`${backlogUrl}/api/v2/issues?apiKey=${apiKey}`)).json();
  console.log(issues);
  dataTable.addRows(-1, issues.map(i => 
  [ 
    i.issueKey, 
    i.summary, 
    i.status.name, 
    i.assignee.name
  ]));
}

interface Space {
  spaceKey: string,
  name: string,
  ownerId: number
  lang: string,
  timezone: string,
  reportSendTime: string,
  textFormattingRule: string,
  created: string,
  updated: string
}
interface IssueType {
  id: number,
  projectId: number,
  name: string,
  color: string,
  displayOrder: number
}
interface Resolution {
  id: number,
  name: string
}
interface Priority {
  id: number,
  name: string
}
interface Status {
  id: number,
  projectId: number,
  name: string,
  color: string,
  displayOrder: number
}
interface User {
  id: number,
  name: string,
  roleType: number,
  lang: string,
  mailAddress: string
}
interface Category {
  id: number,
  name: string,
  displayOrder: number
}
interface Version {
  id: number,
  projectId: number,
  name: string,
  description: string,
  startDate: Date,
  releaseDueDate: Date,
  archived: boolean,
  displayOrder: number
}
interface Attachment {
  id: number,
  name: string,
  size: number
}
interface SharedFile {
  id: number,
  type: string,
  dir: string,
  name: string,
  size: number,
  createdUser: User,
  created: Date,
  updatedUser: User,
  updated: Date
}
interface Star {
  id: number,
  comment: string,
  url: string,
  title: string,
  presenter: User,
  created: Date
}
interface Issue {
  id: number,
  projectId: number,
  issueKey: string,
  keyId: number,
  issueType: IssueType,
  summary: string,
  description: string,
  resolutions: Array<Resolution>,
  priority: Priority,
  status: Status,
  assignee: User,
  category: Array<Category>,
  versions: Array<Version>,
  milestone: Array<Version>,
  startDate: Date,
  dueDate: Date,
  estimatedHours: number,
  actualHours: number,
  parentIssueId: number,
  createdUser: User,
  created: Date,
  updatedUser: User,
  updated: Date,
  customFields
  attachments: Attachment,
  sharedFiles: Array<SharedFile>,
  stars: Array<Star>
}
```