2021-11-25 18:12:12 +01:00
|
|
|
import {
|
|
|
|
DatasourceFieldTypes,
|
|
|
|
Integration,
|
2021-11-25 20:12:32 +01:00
|
|
|
QueryJson,
|
2021-11-25 18:12:12 +01:00
|
|
|
QueryTypes,
|
|
|
|
} from "../definitions/datasource"
|
2022-01-06 09:08:54 +01:00
|
|
|
import { OAuth2Client } from "google-auth-library"
|
2021-11-25 20:12:32 +01:00
|
|
|
import { DatasourcePlus } from "./base/datasourcePlus"
|
2022-01-15 19:28:04 +01:00
|
|
|
import { Row, Table, TableSchema } from "../definitions/common"
|
2021-11-25 20:12:32 +01:00
|
|
|
import { buildExternalTableId } from "./utils"
|
2022-01-15 19:28:04 +01:00
|
|
|
import { DataSourceOperation, FieldTypes } from "../constants"
|
|
|
|
import { GoogleSpreadsheet } from "google-spreadsheet"
|
2022-01-18 17:15:29 +01:00
|
|
|
import { table } from "console"
|
2021-11-25 18:12:12 +01:00
|
|
|
|
|
|
|
module GoogleSheetsModule {
|
2022-01-15 19:28:04 +01:00
|
|
|
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
|
|
|
|
const { getScopedConfig } = require("@budibase/backend-core/db")
|
|
|
|
const { Configs } = require("@budibase/backend-core/constants")
|
|
|
|
|
2021-11-25 18:12:12 +01:00
|
|
|
interface GoogleSheetsConfig {
|
|
|
|
spreadsheetId: string
|
2022-01-06 09:08:54 +01:00
|
|
|
auth: OAuthClientConfig
|
|
|
|
}
|
|
|
|
|
|
|
|
interface OAuthClientConfig {
|
|
|
|
appId: string
|
|
|
|
accessToken: string
|
|
|
|
refreshToken: string
|
2021-11-25 18:12:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const SCHEMA: Integration = {
|
2021-11-25 20:12:32 +01:00
|
|
|
plus: true,
|
2022-01-06 09:08:54 +01:00
|
|
|
auth: {
|
|
|
|
type: "google",
|
|
|
|
},
|
2022-01-26 17:45:28 +01:00
|
|
|
relationships: false,
|
2021-11-25 18:12:12 +01:00
|
|
|
docs: "https://developers.google.com/sheets/api/quickstart/nodejs",
|
|
|
|
description:
|
|
|
|
"Create and collaborate on online spreadsheets in real-time and from any device. ",
|
|
|
|
friendlyName: "Google Sheets",
|
|
|
|
datasource: {
|
|
|
|
spreadsheetId: {
|
|
|
|
type: DatasourceFieldTypes.STRING,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
query: {
|
|
|
|
create: {
|
|
|
|
type: QueryTypes.FIELDS,
|
|
|
|
fields: {
|
|
|
|
sheet: {
|
2022-01-06 09:08:54 +01:00
|
|
|
type: DatasourceFieldTypes.STRING,
|
2021-11-25 18:12:12 +01:00
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
row: {
|
|
|
|
type: QueryTypes.JSON,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
read: {
|
|
|
|
type: QueryTypes.FIELDS,
|
|
|
|
fields: {
|
|
|
|
sheet: {
|
2022-01-06 09:08:54 +01:00
|
|
|
type: DatasourceFieldTypes.STRING,
|
2021-11-25 18:12:12 +01:00
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
update: {
|
|
|
|
type: QueryTypes.FIELDS,
|
|
|
|
fields: {
|
|
|
|
sheet: {
|
2022-01-06 09:08:54 +01:00
|
|
|
type: DatasourceFieldTypes.STRING,
|
2021-11-25 18:12:12 +01:00
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
rowIndex: {
|
2022-01-06 09:08:54 +01:00
|
|
|
type: DatasourceFieldTypes.STRING,
|
2021-11-25 18:12:12 +01:00
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
row: {
|
|
|
|
type: QueryTypes.JSON,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
delete: {
|
|
|
|
type: QueryTypes.FIELDS,
|
|
|
|
fields: {
|
|
|
|
sheet: {
|
2022-01-06 09:08:54 +01:00
|
|
|
type: DatasourceFieldTypes.STRING,
|
2021-11-25 18:12:12 +01:00
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
rowIndex: {
|
2022-01-06 09:08:54 +01:00
|
|
|
type: DatasourceFieldTypes.NUMBER,
|
2021-11-25 18:12:12 +01:00
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-11-25 20:12:32 +01:00
|
|
|
class GoogleSheetsIntegration implements DatasourcePlus {
|
2021-11-25 18:12:12 +01:00
|
|
|
private readonly config: GoogleSheetsConfig
|
|
|
|
private client: any
|
2021-11-25 20:12:32 +01:00
|
|
|
public tables: Record<string, Table> = {}
|
|
|
|
public schemaErrors: Record<string, string> = {}
|
2021-11-25 18:12:12 +01:00
|
|
|
|
|
|
|
constructor(config: GoogleSheetsConfig) {
|
|
|
|
this.config = config
|
2022-01-15 19:28:04 +01:00
|
|
|
const spreadsheetId = this.cleanSpreadsheetUrl(this.config.spreadsheetId)
|
|
|
|
this.client = new GoogleSpreadsheet(spreadsheetId)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pull the spreadsheet ID out from a valid google sheets URL
|
|
|
|
* @param spreadsheetId - the URL or standard spreadsheetId of the google sheet
|
|
|
|
* @returns spreadsheet Id of the google sheet
|
|
|
|
*/
|
|
|
|
cleanSpreadsheetUrl(spreadsheetId: string) {
|
2022-01-18 17:15:29 +01:00
|
|
|
if (!spreadsheetId) {
|
2022-01-26 17:14:47 +01:00
|
|
|
throw new Error(
|
|
|
|
"You must set a spreadsheet ID in your configuration to fetch tables."
|
|
|
|
)
|
2022-01-18 17:15:29 +01:00
|
|
|
}
|
2022-01-15 19:28:04 +01:00
|
|
|
const parts = spreadsheetId.split("/")
|
|
|
|
return parts.length > 5 ? parts[5] : spreadsheetId
|
2021-11-25 18:12:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async connect() {
|
|
|
|
try {
|
2022-01-06 09:08:54 +01:00
|
|
|
// Initialise oAuth client
|
2022-01-15 19:28:04 +01:00
|
|
|
const db = getGlobalDB()
|
|
|
|
const googleConfig = await getScopedConfig(db, {
|
|
|
|
type: Configs.GOOGLE,
|
|
|
|
})
|
2022-01-06 09:08:54 +01:00
|
|
|
const oauthClient = new OAuth2Client({
|
2022-01-15 19:28:04 +01:00
|
|
|
clientId: googleConfig.clientID,
|
|
|
|
clientSecret: googleConfig.clientSecret,
|
2021-11-25 18:12:12 +01:00
|
|
|
})
|
2022-01-06 09:08:54 +01:00
|
|
|
oauthClient.credentials.access_token = this.config.auth.accessToken
|
|
|
|
oauthClient.credentials.refresh_token = this.config.auth.refreshToken
|
|
|
|
this.client.useOAuth2Client(oauthClient)
|
2021-11-25 18:12:12 +01:00
|
|
|
await this.client.loadInfo()
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Error connecting to google sheets", err)
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-15 19:28:04 +01:00
|
|
|
async buildSchema(datasourceId: string) {
|
2021-11-25 20:12:32 +01:00
|
|
|
await this.connect()
|
|
|
|
const sheets = await this.client.sheetsByIndex
|
2022-01-15 19:28:04 +01:00
|
|
|
const tables: Record<string, Table> = {}
|
2021-11-25 20:12:32 +01:00
|
|
|
for (let sheet of sheets) {
|
|
|
|
// must fetch rows to determine schema
|
|
|
|
await sheet.getRows()
|
|
|
|
// build schema
|
2022-01-15 19:28:04 +01:00
|
|
|
const schema: TableSchema = {}
|
2021-11-25 20:12:32 +01:00
|
|
|
|
|
|
|
// build schema from headers
|
|
|
|
for (let header of sheet.headerValues) {
|
|
|
|
schema[header] = {
|
|
|
|
name: header,
|
|
|
|
type: FieldTypes.STRING,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// create tables
|
|
|
|
tables[sheet.title] = {
|
|
|
|
_id: buildExternalTableId(datasourceId, sheet.title),
|
|
|
|
name: sheet.title,
|
|
|
|
primary: ["rowNumber"],
|
|
|
|
schema,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.tables = tables
|
|
|
|
}
|
|
|
|
|
|
|
|
async query(json: QueryJson) {
|
|
|
|
const sheet = json.endpoint.entityId
|
|
|
|
|
2022-01-18 17:15:29 +01:00
|
|
|
const handlers = {
|
2022-01-26 17:14:47 +01:00
|
|
|
[DataSourceOperation.CREATE]: () =>
|
|
|
|
this.create({ sheet, row: json.body }),
|
2022-01-18 17:15:29 +01:00
|
|
|
[DataSourceOperation.READ]: () => this.read({ sheet }),
|
2022-01-26 17:14:47 +01:00
|
|
|
[DataSourceOperation.UPDATE]: () =>
|
|
|
|
this.update({
|
|
|
|
// exclude the header row and zero index
|
|
|
|
rowIndex: json.extra?.idFilter?.equal?.rowNumber - 2,
|
|
|
|
sheet,
|
|
|
|
row: json.body,
|
|
|
|
}),
|
|
|
|
[DataSourceOperation.DELETE]: () =>
|
|
|
|
this.delete({
|
|
|
|
// exclude the header row and zero index
|
|
|
|
rowIndex: json.extra?.idFilter?.equal?.rowNumber - 2,
|
|
|
|
sheet,
|
|
|
|
}),
|
|
|
|
[DataSourceOperation.CREATE_TABLE]: () =>
|
|
|
|
this.createTable(json?.table?.name),
|
2022-01-18 17:15:29 +01:00
|
|
|
[DataSourceOperation.UPDATE_TABLE]: () => this.updateTable(json.table),
|
2022-01-26 17:14:47 +01:00
|
|
|
[DataSourceOperation.DELETE_TABLE]: () =>
|
|
|
|
this.deleteTable(json?.table?.name),
|
2021-11-25 20:12:32 +01:00
|
|
|
}
|
2022-01-18 17:15:29 +01:00
|
|
|
|
|
|
|
const internalQueryMethod = handlers[json.endpoint.operation]
|
|
|
|
|
|
|
|
return await internalQueryMethod()
|
2021-11-25 20:12:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
buildRowObject(headers: string[], values: string[], rowNumber: number) {
|
2022-01-15 19:28:04 +01:00
|
|
|
const rowObject: { rowNumber: number; [key: string]: any } = { rowNumber }
|
2021-11-25 18:12:12 +01:00
|
|
|
for (let i = 0; i < headers.length; i++) {
|
2022-01-15 19:28:04 +01:00
|
|
|
rowObject._id = rowNumber
|
2021-11-25 18:12:12 +01:00
|
|
|
rowObject[headers[i]] = values[i]
|
|
|
|
}
|
|
|
|
return rowObject
|
|
|
|
}
|
|
|
|
|
2022-01-18 17:15:29 +01:00
|
|
|
async createTable(name?: string) {
|
|
|
|
try {
|
|
|
|
await this.connect()
|
2022-01-26 17:14:47 +01:00
|
|
|
const sheet = await this.client.addSheet({ title: name })
|
2022-01-18 17:15:29 +01:00
|
|
|
return sheet
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Error creating new table in google sheets", err)
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateTable(table?: any) {
|
|
|
|
try {
|
|
|
|
await this.connect()
|
|
|
|
const sheet = await this.client.sheetsByTitle[table.name]
|
|
|
|
await sheet.loadHeaderRow()
|
|
|
|
|
|
|
|
if (table._rename) {
|
|
|
|
const headers = []
|
|
|
|
for (let header of sheet.headerValues) {
|
|
|
|
if (header === table._rename.old) {
|
|
|
|
headers.push(table._rename.updated)
|
|
|
|
} else {
|
|
|
|
headers.push(header)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await sheet.setHeaderRow(headers)
|
|
|
|
} else {
|
2022-01-26 17:14:47 +01:00
|
|
|
let newField = Object.keys(table.schema).find(
|
|
|
|
key => !sheet.headerValues.includes(key)
|
|
|
|
)
|
2022-01-18 17:15:29 +01:00
|
|
|
await sheet.setHeaderRow([...sheet.headerValues, newField])
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Error updating table in google sheets", err)
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-18 17:41:38 +01:00
|
|
|
async deleteTable(sheet: any) {
|
2022-01-18 17:15:29 +01:00
|
|
|
try {
|
|
|
|
await this.connect()
|
2022-01-18 17:41:38 +01:00
|
|
|
const sheetToDelete = await this.client.sheetsByTitle[sheet]
|
|
|
|
return await sheetToDelete.delete()
|
2022-01-18 17:15:29 +01:00
|
|
|
} catch (err) {
|
|
|
|
console.error("Error deleting table in google sheets", err)
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-15 19:28:04 +01:00
|
|
|
async create(query: { sheet: string; row: any }) {
|
2021-11-25 18:12:12 +01:00
|
|
|
try {
|
|
|
|
await this.connect()
|
|
|
|
const sheet = await this.client.sheetsByTitle[query.sheet]
|
2021-11-25 20:12:32 +01:00
|
|
|
const rowToInsert =
|
|
|
|
typeof query.row === "string" ? JSON.parse(query.row) : query.row
|
2021-11-25 18:12:12 +01:00
|
|
|
const row = await sheet.addRow(rowToInsert)
|
2021-11-25 20:12:32 +01:00
|
|
|
return [
|
|
|
|
this.buildRowObject(sheet.headerValues, row._rawData, row._rowNumber),
|
|
|
|
]
|
2021-11-25 18:12:12 +01:00
|
|
|
} catch (err) {
|
|
|
|
console.error("Error writing to google sheets", err)
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async read(query: { sheet: string }) {
|
|
|
|
try {
|
|
|
|
await this.connect()
|
|
|
|
const sheet = await this.client.sheetsByTitle[query.sheet]
|
|
|
|
const rows = await sheet.getRows()
|
|
|
|
const headerValues = sheet.headerValues
|
|
|
|
const response = []
|
|
|
|
for (let row of rows) {
|
2021-11-25 20:12:32 +01:00
|
|
|
response.push(
|
|
|
|
this.buildRowObject(headerValues, row._rawData, row._rowNumber)
|
|
|
|
)
|
2021-11-25 18:12:12 +01:00
|
|
|
}
|
|
|
|
return response
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Error reading from google sheets", err)
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-15 19:28:04 +01:00
|
|
|
async update(query: { sheet: string; rowIndex: number; row: any }) {
|
2021-11-25 18:12:12 +01:00
|
|
|
try {
|
|
|
|
await this.connect()
|
|
|
|
const sheet = await this.client.sheetsByTitle[query.sheet]
|
|
|
|
const rows = await sheet.getRows()
|
|
|
|
const row = rows[query.rowIndex]
|
|
|
|
if (row) {
|
2022-01-15 19:28:04 +01:00
|
|
|
const updateValues = query.row
|
2021-11-25 18:12:12 +01:00
|
|
|
for (let key in updateValues) {
|
|
|
|
row[key] = updateValues[key]
|
|
|
|
}
|
|
|
|
await row.save()
|
2021-11-25 20:12:32 +01:00
|
|
|
return [
|
|
|
|
this.buildRowObject(
|
|
|
|
sheet.headerValues,
|
|
|
|
row._rawData,
|
|
|
|
row._rowNumber
|
|
|
|
),
|
|
|
|
]
|
2021-11-25 18:12:12 +01:00
|
|
|
} else {
|
|
|
|
throw new Error("Row does not exist.")
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error("Error reading from google sheets", err)
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async delete(query: { sheet: string; rowIndex: number }) {
|
|
|
|
await this.connect()
|
|
|
|
const sheet = await this.client.sheetsByTitle[query.sheet]
|
|
|
|
const rows = await sheet.getRows()
|
|
|
|
const row = rows[query.rowIndex]
|
|
|
|
if (row) {
|
|
|
|
await row.delete()
|
|
|
|
return [{ deleted: query.rowIndex }]
|
|
|
|
} else {
|
|
|
|
throw new Error("Row does not exist.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
schema: SCHEMA,
|
|
|
|
integration: GoogleSheetsIntegration,
|
|
|
|
}
|
|
|
|
}
|