Cloud FunctionsのTypeScriptからGoogleSheetsのデータを取得する2019

わざわざ管理画面を作るのめんどいからGoogleSheetsで済ませたい、というのは稀に良くある状況だと思います。

![dogs](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47823/91067560-c554-f8e5-1ac9-c0b057ff94d3.png)

みたいなスプレッドシートで管理されたデータが欲しかったとして

https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get

Google Sheets APIを利用すればCloud Functionsに限らずどこからでも何からでもアクセスは出来るんですが、Cloud Functions等の場合はデフォルトのサービスアカウントを利用する事でちょっと楽をできます。

```json:package.json
"dependencies": {
    ...,
    "googleapis": "^39.2.0",
}
```

```typescript:GoogleSpreadSheets.ts
import * as gapis from 'googleapis';

export class GoogleSpreadSheets {

    private auth = async (): Promise<gapis.sheets_v4.Sheets> => {
        const client = await gapis.google.auth.getClient({
            scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
        });
        return new gapis.sheets_v4.Sheets({ auth: client });
    }

    fetchValueRange = async (spreadsheetId: string, tabTitle: string): Promise<gapis.sheets_v4.Schema$ValueRange> => {
        const authed = await this.auth();
        const res = await authed.spreadsheets.values.get({ spreadsheetId: spreadsheetId, range: tabTitle });
        return res.data;
    }
}
```
spreadsheetIdというのはシートのURLにおける

```
https://docs.google.com/spreadsheets/d/${spreadsheetId}/
```

の部分で、rangeには今回の場合はタブ名を指定してますがA1:B1とかそういう書式が使えます。余談ですがGoogle Sheets APIを使っていると、Sheetsとは何かSheetとは何かみたいな迷宮へいざなわれます。

で、APIから返ってくるValueRangeを

```typescript:GoogleSheetData.ts
import * as entity from '../Entity';

export class GoogleSheetData {

    private sheet: google.sheets_v4.Schema$ValueRange;
    private rows: any[];

    constructor(sheet: google.sheets_v4.Schema$ValueRange) {
        this.sheet = sheet;
        // 先頭行を飛ばしてる
        this.rows = this.rowRange().slice(1).map(this.rowValuesAt);
    }

    toDogs = (): entity.Dog[] => {
        return this.rows.map(this.toDog).filter(utils.valueIsExist);
    }

    private toDog = (row: any[]): entity.Dog | undefined => {
        const data: entity.DogData = {
            name: row[0],
            race: row[1],
        };
        if (!entity.dataIsDogData(data)) {
            console.error('GoogleSheetData.toDog %O is not DogData', data);
            return undefined;
        }
        return new entity.Dog(data);
    }

    private rowValuesAt = (i: number): any[] => {
        if (this.sheet.values === undefined) {
            return [];
        }
        return this.sheet.values[i];
    }

    private rowCount = (): number => {
        if (this.sheet.values === undefined) {
            return 0;
        }
        return this.sheet.values.length;
    }

    private rowRange = (): number[] => {
        return this.range(this.rowCount());
    }

    private range = (l: number): number[] => {
        return Array.from({ length: l }, (v, i) => i);
    };
```
[とか何とかしてやる](https://qiita.com/oubakiou/items/aa339796ed9d14f4d6ef)とシートから大量の犬を生成するコードが出来ます。でデプロイした後、最後にこのfunctionのサービスアカウントに目的のシートへのアクセス権をつけてやる必要があります。

https://console.cloud.google.com/functions/

から目的のfunctionの詳細を表示すれば利用している「サービスアカウント」の名前がわかる(Cloud Functions for Firebaseの場合も実体はCloud Functionsなので同様)ので、目的のシートを開いて右上にある共有ボタンから
![共有ボタン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47823/33185323-aa85-b72d-c9dd-d8f68fe2e5a4.png)
さっき確認したサービスアカウント名を入力して招待すれば該当シートへのアクセス権が付与されます。
![招待](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47823/6efdbff7-0ef3-ebe5-f674-71ff7f0e6ae7.png)

また権限以前にGoogle Sheets API自体を
https://console.cloud.google.com/apis/library
から有効状態にしておく必要もあります。

留意点としてGoogle Sheets APIはそこそこレスポンスが遅いので、それが問題になるようであれば[1日1回とか](https://firebase.google.com/docs/functions/schedule-functions)[FireStore](https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja#custom_objects)にでも犬達を突っ込んでおけば良いと思います。