Google Sheets Connector

Reshuffle Google Sheets Connector

npm install reshuffle-google-connectors

This is a Reshuffle connector that allows you to Interact with online Google Sheets.

The connector is designed to work with Google Sheets in the form of a table, where the header row (first row) is defined.

The following example listens to changes in online spreadsheet:

const { GoogleSheetsConnector } = require('reshuffle-google-connectors')

const myGoogleSheetsConnector = new GoogleSheetsConnector(app, {
  credentials: {
    client_email: '<your_client_email>',
    private_key: '<your_private_key>',
  },
  documentId: '<your_documentId>',
})

const myHandler = (event, app) => {
  // event is { oldRows, newRows, worksheetsRemoved: WorkSheetChanges[], worksheetsAdded: WorkSheetChanges[], worksheetsChanged: WorkSheetChanges[] }
  // WorkSheetChanges is { worksheetId, rowsRemoved, rowsAdded, rowsChanged }
  console.log('New rows detected!')
  event.options.sheetIdOrTitle &&
    console.log(
      `'sheetIdOrTitle' is set in event options so it only checks for changes in sheet ${event.options.sheetIdOrTitle}`,
    )

  event.newRows.forEach(({ worksheetId, rows }) => {
    console.log(`workSheetId: ${worksheetId}`)

    rows.forEach((row, index) => {
      let rowString = `line ${index + 1}\t`
      Object.values(row).forEach((val) => (rowString += `${val}\t`))
      console.log(rowString)
    })
  })

  event.worksheetsChanged[0] &&
    event.worksheetsChanged[0].rowsAdded[0] &&
    console.log(
      `Example of new line values ${JSON.stringify(
        event.worksheetsChanged[0].rowsAdded[0],
      )}`,
    )
}

/** Trigger a handler when changes are detected in document <sheetId> (it will check for changes every 10 seconds) */
myGoogleSheetsConnector.on({}, myHandler)

app.start()

Configuration Options:

interface GoogleSheetsConnectorConfigOptions {
  credentials: ServiceAccountCredentials
  sheetId: string
}

Connector events

rows changed

This event is fired when rows have changed in the online Sheets.

You can trigger a handler on rows changed by providing the following options to the on method:

interface GoogleSheetsConnectorEventOptions {
  interval?: number
  sheetIdOrTitle?: string
  keyColumn?: string
}

Event is an object containing attribute:

oldRows: cellValue[]
newRows: cellValue[]
worksheetsRemoved: WorkSheetChanges[]
worksheetsAdded: WorkSheetChanges[]
worksheetsChanged: WorkSheetChanges[]

export interface cellValue {
  headerName: string
  value: string
}

export interface WorkSheetChanges {
  worksheetId: string
  rowsRemoved: any[]
  rowsAdded: any[]
  rowsChanged: RowChange[]
}

export interface RowChange {
  prev: any
  curr: any
}
Example:
const myHandler = (event, app) => {
  console.log('New rows detected!')
  event.options.sheetIdOrTitle && console.log(`'sheetIdOrTitle' is set in event options so it only checks for changes in sheet ${event.options.sheetIdOrTitle}`)

  event.newRows.forEach(({worksheetId, rows}) => {
    console.log(`workSheetId: ${worksheetId}`)

    rows.forEach((row, index) => {
      let rowString = `line ${index + 1}\t`
      Object.values(row).forEach(val => rowString += `${val}\t`)
      console.log(rowString)
    })
  })

  event.worksheetsChanged[0]
    && event.worksheetsChanged[0].rowsAdded[0]
    && console.log(`Example of new line values ${JSON.stringify(event.worksheetsChanged[0].rowsAdded[0])}`)
}

/** Trigger a handler when changes are detected in document <sheetId> (it will check for changes every 10 seconds) */
myGoogleSheetsConnector.on({}, myHandler)

/** Check for changes every minute (it overrides the default timer set to 10 sec) */
const aMinuteMs = 60 * 1000
myGoogleSheetsConnector.on({interval: aMinuteMs}, myHandler)

/** Check for changes in a specific sheet by id */
myGoogleSheetsConnector.on({sheetIdOrTitle: 0}, myHandler)

/** Check for changes only in a specific sheet by title and every 30 seconds */
myGoogleSheetsConnector.on({sheetIdOrTitle: 'Sheet1', interval: 30 * 1000}, myHandler)

/** Check for changes using a key column for more accuracy, see Known Issues below */
myGoogleSheetsConnector.on({keyColumn: 'Identifier'}, myHandler)
Known Issues:

Since Google Sheets does not provide row identifiers, it is hard to identify which events caused the changes, for example when deleting a row which is not the last one in the spreadsheet, the rows below are shifted up. This can be interpreted as an update of all the rows below including the "deleted" row and a deletion of the last row. These are the options to handle such cases: 1. Add or delete rows just from the bottom of the spreadsheet. 2. Treat event.oldRows and event.newRows as before and after snapshots. You may compare these yourself to ascertain which changes your code is interested in. 3. Set a Key column in your spreadsheet with a unique identifier and provide its name as the GoogleSheetsConnectorEventOptions.keyColumn. When this option is used, the connector can differentiate added and deleted rows based on these unique identifiers. This removes the ambiguity described above. Note that these identifiers must be unique. If duplicates exist in your spreadsheet, the connector will not behave as expected.

When using the keyColumn and having multiple sheets, the same column name should be used in all the sheets. In every sheet the column can have different types of identifiers but the column name should be the same, for example keyColumn is set to be 'RS-Identifier', in Sheet1 it can be email address while in Sheet2 it can be a serial number.

Connector actions

This connector provides all the common actions to interact with a Google Sheets document. It also provides the sdk (which return a Google Sheets instance) for more advanced operations.

getRows

Get all rows of a sheet id|title. This action returns an array of rows. Each row is an object, with properties matching the column names set by the header row.

Example
const sheetTitle = 'Sheet1'
const rows = await myGoogleSheetsConnector.getRows(sheetTitle)
console.log(`Sheet ${sheetTitle} contains ${rows.length} row(s)`)
rows.forEach(row => console.log(row._rawData))
getCell

Get data of a cell at rowIndex/columnIndex of a sheet id|title

Example
const rowIndex = 2
const columnIndex = 1
const cell = await myGoogleSheetsConnector.getCell(sheetId, rowIndex, columnIndex)
console.log(`Cell details [value: ${cell.value}, a1Address: ${cell.a1Address},` +
   ` rowIndex: ${cell.rowIndex}, columnIndex: ${cell.columnIndex}, a1Row: ${cell.a1Row}, a1Column:${cell.a1Column}]`)
getCellByA1

Get data of a cell by a1 address of a sheet id|title

Example
const a1Address = 'B2'
const cell = await myGoogleSheetsConnector.getCellByA1(sheetTitle, a1Address)
console.log(`Cell has properties ${JSON.stringify(cell)}`)
getCells

Get cells data for range in sheet by id|title

Example
const range = {startRowIndex: 1, endRowIndex: 5, startColumnIndex: 0, endColumnIndex: 2}
const rows = await myGoogleSheetsConnector.getCells(sheetTitle, range)
console.log(`Sheets contains ${rows.length} row(s)`)
rows.forEach(row => console.log(row))
addRow

Adds a new row to a sheet identified by its id|title

Example
const sheetId = 0
const values = ['value 1', 'value 2', 'value 3']
await myGoogleSheetsConnector.addRow(sheetId, values)
getInfo

Get document info (returns an object with sheetCount and sheetsByIndex)

Example
const docInfo = await myGoogleSheetsConnector.getInfo()
console.log(`Document has ${docInfo.sheetCount} sheets.`)
docInfo.sheetsByIndex.forEach(
  sheetDetails => console.log(`[id: ${sheetDetails.sheetId}, title: ${sheetDetails.title}]`)
)
sdk

returns a Google Sheets instance for sheetId

See documentation in github: https://github.com/theoephraim/node-google-spreadsheet

Examples
/** Add new sheet using the sdk */
const newSheetTitle = 'new sheet'
const newSheetProperties = {title: newSheetTitle, headerValues: ['header 1', 'header 2', 'header 3']}
const doc = await myGoogleSheetsConnector.sdk()
await doc.addSheet(newSheetProperties)

/** Delete sheet by id|title using the sdk */
const sheetTitleToDelete = 'new sheet'
const doc = await myGoogleSheetsConnector.sdk()
const sheet = doc.sheetsByTitle[sheetTitleToDelete]
// const sheet = doc.sheetsByIndex[2] // Use this line for deleting by sheet id
await sheet.delete()

More examples on how to use this connector can be found here.