Google Sheets APIでシートの最終行を取得(Nodejs)

# 結論
GASのgetLastRowのようなメソッドは無いので、
[spreadsheets.values.append](https://developers.google.com/sheets/api/guides/values#appending_values) で空の行を追加し戻り値から最終行を取得すればOKです。


## セットアップ
① [GCP](https://console.cloud.google.com/home/dashboard) で [Sheets API](https://console.cloud.google.com/marketplace/product/google/sheets.googleapis.com) を有効化
② [Credentials](https://console.cloud.google.com/apis/credentials) ページで OAuth Client ID を作成（Application Typeは Desktop app を選択）し、client_secret ファイルを `credentials.json` という名前で保存
③ google api パッケージをインストール

``` terminal
npm install googleapis@39 --save
```

## コード
下記のファイルをコピペして、credentials.jsonと同じ場所に保存し実行してください。
※SPREADSHEET_ID と SHEET_NAME はご自身のものに変更してください）
※認証部分のコードは [Google公式のquickstart](https://developers.google.com/sheets/api/quickstart/nodejs) と全く同じです。

``` getLastRow.js
const fs = require('fs');
const readline = require('readline');
const { google } = require('googleapis');

//最終行を取得したいシートの名前とスプレッドシートIDを指定=====

const SPREADSHEET_ID = '*************************';
const SHEET_NAME = 'シート1';

//====================================================

const SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];
const CLIENT_SECRET_PATH = 'credentials.json';
const TOKEN_PATH = 'token.json'; // トークンファイル名（トークンファイルは初めてこのファイルを実行する際に自動生成される）


fs.readFile(CLIENT_SECRET_PATH, (err, content) => {
    if (err) return console.log('Error loading client secret file:', err);
    authorize(JSON.parse(content), getLastRow);
});

/**
認証関数　既に認証されていればcallbackのみ実行
 */
function authorize(credentials, callback) {
    const { client_secret, client_id, redirect_uris } = credentials.installed;
    const oAuth2Client = new google.auth.OAuth2(
        client_id, client_secret, redirect_uris[0]);

    // Check if we have previously stored a token.
    fs.readFile(TOKEN_PATH, (err, token) => {
        if (err) return getNewToken(oAuth2Client, callback);
        oAuth2Client.setCredentials(JSON.parse(token));
        callback(oAuth2Client);
    });
}

/**
トークン生成関数　既にtokenファイルがあればcallbackのみ実行
 */
function getNewToken(oAuth2Client, callback) {
    const authUrl = oAuth2Client.generateAuthUrl({
        access_type: 'offline',
        scope: SCOPES,
    });
    console.log('Authorize this app by visiting this url:', authUrl);
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
    });
    rl.question('Enter the code from that page here: ', (code) => {
        rl.close();
        oAuth2Client.getToken(code, (err, token) => {
            if (err) return console.error('Error while trying to retrieve access token', err);
            oAuth2Client.setCredentials(token);
            // Store the token to disk for later program executions
            fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
                if (err) return console.error(err);
                console.log('Token stored to', TOKEN_PATH);
            });
            callback(oAuth2Client);
        });
    });
}

/**
最終行を取得する関数
 */
async function getLastRow(auth) {
    const sheets = google.sheets({ version: 'v4', auth });
    const lastRow = await new Promise((resolve, reject) => {
        sheets.spreadsheets.values.append({
            spreadsheetId: SPREADSHEET_ID,
            range: `${SHEET_NAME}!A1`,
            valueInputOption: 'USER_ENTERED',
            insertDataOption: 'INSERT_ROWS',
            resource: {
                values: [
                    [],
                ],
            }
        }, (err, result) => {
            if (err) {
                // Handle error.
                console.log(err);
            } else {
                //result.data.tableRangeが無い = シートのどのセルにも値が入力されていないので、0を返す
                if(!result.data.tableRange) return resolve(0);
                const match = result.data.tableRange.match(/^.*![A-Z]+\d+:[A-Z]+(?<lastRow>\d+)$/);
                if(match){
                    resolve(match.groups.lastRow);
                } else {
                    //1つのセルのみ入力されているシートの場合は上記の正規表現ではエラーになる
                    const match_only1cell = result.data.tableRange.match(/^.*![A-Z]+(?<lastRow>\d+)$/);
                    resolve(match_only1cell.groups.lastRow);
                }
            }
        })
    });
    
    //最終行を出力
    console.log(`lastRow: ${lastRow}`);
}
```

## 解説
`sheets.spreadsheets.values.append` の result.data は下記のようになっています。
tableRangeが空行追加前の既存データ範囲なので、ここの範囲の末尾を正規表現で抜き出すことで最終行を取得できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181210/1cd58860-f442-948e-6f45-968e06017923.png)

# おまけ　GetValues・SetValuesも搭載したクラス
```GoogleSheetsClass.js
const fs = require('fs');
const readline = require('readline');
const { google } = require('googleapis');

class GoogleSheetsClass {
    constructor() {
        this.SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];
        this.sheets = null;
        //認証しsheetsオブジェクトを取得しておく
        fs.readFile(CLIENT_SECRET_PATH, (err, content) => {
            if (err) return console.log('Error loading client secret file:', err);
            this.authorize(JSON.parse(content), auth => {
                this.sheets = google.sheets({ version: 'v4', auth });
            });
        });
    }

    /**
    認証関数　既に認証されていればcallbackのみ実行
    */
    authorize(credentials, callback) {
        const { client_secret, client_id, redirect_uris } = credentials.installed;
        const oAuth2Client = new google.auth.OAuth2(
            client_id, client_secret, redirect_uris[0]);

        // Check if we have previously stored a token.
        fs.readFile(TOKEN_PATH, (err, token) => {
            if (err) return this.getNewToken(oAuth2Client, callback);
            oAuth2Client.setCredentials(JSON.parse(token));
            callback(oAuth2Client);
        });
    }

    /**
    トークン生成関数　既にtokenファイルがあればcallbackのみ実行
    */
    getNewToken(oAuth2Client, callback) {
        const authUrl = oAuth2Client.generateAuthUrl({
            access_type: 'offline',
            scope: this.SCOPES,
        });
        console.log('Authorize this app by visiting this url:', authUrl);
        const rl = readline.createInterface({
            input: process.stdin,
            output: process.stdout,
        });
        rl.question('Enter the code from that page here: ', (code) => {
            rl.close();
            oAuth2Client.getToken(code, (err, token) => {
                if (err) return console.error('Error while trying to retrieve access token', err);
                oAuth2Client.setCredentials(token);
                // Store the token to disk for later program executions
                fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
                    if (err) return console.error(err);
                    console.log('Token stored to', TOKEN_PATH);
                });
                callback(oAuth2Client);
            });
        });
    }

    //getLastRow
    async getLastRow(spreadsheet_id, sheet_name) {
        return new Promise((resolve, reject) => {
            this.sheets.spreadsheets.values.append({
                spreadsheetId: spreadsheet_id,
                range: `'${sheet_name}'!A1`,
                valueInputOption: 'USER_ENTERED',
                insertDataOption: 'INSERT_ROWS',
                resource: {
                    values: [
                        [],
                    ],
                }
            }, (err, result) => {
                if (err) {
                    // Handle error.
                    throw err;
                } else {
                    //result.data.tableRangeが無い = シートのどのセルにも値が入力されていないので、0を返す
                    if(!result.data.tableRange) return resolve(0);
                    const match = result.data.tableRange.match(/^.*![A-Z]+\d+:[A-Z]+(?<lastRow>\d+)$/);
                    if(match){
                        resolve(match.groups.lastRow);
                    } else {
                        //1つのセルのみ入力されているシートの場合は上記の正規表現ではエラーになる
                        const match_only1cell = result.data.tableRange.match(/^.*![A-Z]+(?<lastRow>\d+)$/);
                        resolve(match_only1cell.groups.lastRow);
                    }
                }
            })
        });
    }
    
    /*
    ** getValues
    ** rangeは"A1:B2"形式で指定
    */
    async getValues(spreadsheet_id, sheet_name, range){
        return new Promise((resolve, reject) => {
            this.sheets.spreadsheets.values.get({
                spreadsheetId: spreadsheet_id,
                range: `'${sheet_name}'!${range}`,
            }, (err, result) => {
                if (err) { throw err; } else {
                    resolve(result.data.values);
                }
            });
        });
    }

    /*
    ** setValues
    ** rangeは"A1:B2"形式、 valuesは二次元配列を指定
    */
    async setValues(spreadsheet_id, sheet_name, range, values){
        return new Promise((resolve, reject) => {
            this.sheets.spreadsheets.values.update({
                spreadsheetId: spreadsheet_id,
                range: `'${sheet_name}'!${range}`,
                valueInputOption: "USER_ENTERED",
                resource : {
                    values : values //[[]]
                }
            }, (err, result) => {
                if (err) { throw err; } else {
                    resolve(result.data);
                }
            });
        });
    }
}

```