Docs to Google Sheets mock.

This commit is contained in:
Sam Rose 2024-09-09 12:05:15 +01:00
parent 1bc84c1633
commit 1c5b50773f
No known key found for this signature in database
1 changed files with 144 additions and 34 deletions

View File

@ -2,7 +2,10 @@ import { Datasource } from "@budibase/types"
import nock from "nock" import nock from "nock"
import { GoogleSheetsConfig } from "../../googlesheets" import { GoogleSheetsConfig } from "../../googlesheets"
// https://protobuf.dev/reference/protobuf/google.protobuf/#value
type Value = string | number | boolean type Value = string | number | boolean
// https://developers.google.com/sheets/api/reference/rest/v4/Dimension
type Dimension = "ROWS" | "COLUMNS" type Dimension = "ROWS" | "COLUMNS"
interface Range { interface Range {
@ -18,12 +21,14 @@ interface DimensionProperties {
// dataSourceColumnReference: DataSourceColumnReference // dataSourceColumnReference: DataSourceColumnReference
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values#ValueRange
interface ValueRange { interface ValueRange {
range: string range: string
majorDimension: Dimension majorDimension: Dimension
values: Value[][] values: Value[][]
} }
// https://developers.google.com/sheets/api/reference/rest/v4/UpdateValuesResponse
interface UpdateValuesResponse { interface UpdateValuesResponse {
spreadsheetId: string spreadsheetId: string
updatedRange: string updatedRange: string
@ -33,6 +38,7 @@ interface UpdateValuesResponse {
updatedData: ValueRange updatedData: ValueRange
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/response#AddSheetResponse
interface AddSheetResponse { interface AddSheetResponse {
properties: SheetProperties properties: SheetProperties
} }
@ -41,12 +47,14 @@ interface Response {
addSheet?: AddSheetResponse addSheet?: AddSheetResponse
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/response
interface BatchUpdateResponse { interface BatchUpdateResponse {
spreadsheetId: string spreadsheetId: string
replies: Response[] replies: Response[]
updatedSpreadsheet: Spreadsheet updatedSpreadsheet: Spreadsheet
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#GridProperties
interface GridProperties { interface GridProperties {
rowCount: number rowCount: number
columnCount: number columnCount: number
@ -57,12 +65,14 @@ interface GridProperties {
columnGroupControlAfter: boolean columnGroupControlAfter: boolean
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#SheetProperties
interface SheetProperties { interface SheetProperties {
sheetId: number sheetId: number
title: string title: string
gridProperties: GridProperties gridProperties: GridProperties
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddSheetRequest
interface AddSheetRequest { interface AddSheetRequest {
properties: SheetProperties properties: SheetProperties
} }
@ -71,6 +81,7 @@ interface Request {
addSheet?: AddSheetRequest addSheet?: AddSheetRequest
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request
interface BatchUpdateRequest { interface BatchUpdateRequest {
requests: Request[] requests: Request[]
includeSpreadsheetInResponse: boolean includeSpreadsheetInResponse: boolean
@ -78,11 +89,13 @@ interface BatchUpdateRequest {
responseIncludeGridData: boolean responseIncludeGridData: boolean
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ErrorValue
interface ErrorValue { interface ErrorValue {
type: string type: string
message: string message: string
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ExtendedValue
interface ExtendedValue { interface ExtendedValue {
stringValue?: string stringValue?: string
numberValue?: number numberValue?: number
@ -91,14 +104,17 @@ interface ExtendedValue {
errorValue?: ErrorValue errorValue?: ErrorValue
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#CellData
interface CellData { interface CellData {
userEnteredValue: ExtendedValue userEnteredValue: ExtendedValue
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#RowData
interface RowData { interface RowData {
values: CellData[] values: CellData[]
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#GridData
interface GridData { interface GridData {
startRow: number startRow: number
startColumn: number startColumn: number
@ -107,21 +123,61 @@ interface GridData {
columnMetadata: DimensionProperties[] columnMetadata: DimensionProperties[]
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#Sheet
interface Sheet { interface Sheet {
properties: SheetProperties properties: SheetProperties
data: GridData[] data: GridData[]
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#SpreadsheetProperties
interface SpreadsheetProperties { interface SpreadsheetProperties {
title: string title: string
} }
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#Spreadsheet
interface Spreadsheet { interface Spreadsheet {
properties: SpreadsheetProperties properties: SpreadsheetProperties
spreadsheetId: string spreadsheetId: string
sheets: Sheet[] sheets: Sheet[]
} }
// https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption
type ValueInputOption =
| "USER_ENTERED"
| "RAW"
| "INPUT_VALUE_OPTION_UNSPECIFIED"
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append#InsertDataOption
type InsertDataOption = "OVERWRITE" | "INSERT_ROWS"
// https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption
type ValueRenderOption = "FORMATTED_VALUE" | "UNFORMATTED_VALUE" | "FORMULA"
// https://developers.google.com/sheets/api/reference/rest/v4/DateTimeRenderOption
type DateTimeRenderOption = "SERIAL_NUMBER" | "FORMATTED_STRING"
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append#query-parameters
interface AppendParams {
valueInputOption?: ValueInputOption
insertDataOption?: InsertDataOption
includeValuesInResponse?: boolean
responseValueRenderOption?: ValueRenderOption
responseDateTimeRenderOption?: DateTimeRenderOption
}
interface AppendRequest {
range: string
params: AppendParams
body: ValueRange
}
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append#response-body
interface AppendResponse {
spreadsheetId: string
tableRange: string
updates: UpdateValuesResponse
}
export class GoogleSheetsMock { export class GoogleSheetsMock {
private config: GoogleSheetsConfig private config: GoogleSheetsConfig
private spreadsheet: Spreadsheet private spreadsheet: Spreadsheet
@ -141,7 +197,38 @@ export class GoogleSheetsMock {
} }
} }
init() { private route(
method: "get" | "put" | "post",
path: string | RegExp,
handler: (uri: string, request: nock.Body) => nock.Body
): nock.Scope {
const headers = { reqheaders: { authorization: "Bearer test" } }
const scope = nock("https://sheets.googleapis.com/", headers)
return scope[method](path).reply(200, handler).persist()
}
private get(
path: string | RegExp,
handler: (uri: string, request: nock.Body) => nock.Body
): nock.Scope {
return this.route("get", path, handler)
}
private put(
path: string | RegExp,
handler: (uri: string, request: nock.Body) => nock.Body
): nock.Scope {
return this.route("put", path, handler)
}
private post(
path: string | RegExp,
handler: (uri: string, request: nock.Body) => nock.Body
): nock.Scope {
return this.route("post", path, handler)
}
private mockAuth() {
nock("https://www.googleapis.com/") nock("https://www.googleapis.com/")
.post("/oauth2/v4/token") .post("/oauth2/v4/token")
.reply(200, { .reply(200, {
@ -164,50 +251,72 @@ export class GoogleSheetsMock {
scopes: "https://www.googleapis.com/auth/spreadsheets", scopes: "https://www.googleapis.com/auth/spreadsheets",
}) })
.persist() .persist()
}
nock("https://sheets.googleapis.com/", { init() {
reqheaders: { authorization: "Bearer test" }, this.mockAuth()
})
.get(`/v4/spreadsheets/${this.config.spreadsheetId}/`)
.reply(200, () => this.handleGetSpreadsheet())
.persist()
nock("https://sheets.googleapis.com/", { this.get(`/v4/spreadsheets/${this.config.spreadsheetId}/`, () =>
reqheaders: { authorization: "Bearer test" }, this.handleGetSpreadsheet()
}) )
.post(`/v4/spreadsheets/${this.config.spreadsheetId}/:batchUpdate`)
.reply(200, (_uri, request) =>
this.handleBatchUpdate(request as BatchUpdateRequest)
)
.persist()
nock("https://sheets.googleapis.com/", { // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate
reqheaders: { authorization: "Bearer test" }, this.post(
}) `/v4/spreadsheets/${this.config.spreadsheetId}/:batchUpdate`,
.put( (_uri, request) => this.handleBatchUpdate(request as BatchUpdateRequest)
new RegExp(`/v4/spreadsheets/${this.config.spreadsheetId}/values/.*`) )
)
.reply(200, (_uri, request) =>
this.handleValueUpdate(request as ValueRange)
)
.persist()
nock("https://sheets.googleapis.com/", { // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/update
reqheaders: { authorization: "Bearer test" }, this.put(
}) new RegExp(`/v4/spreadsheets/${this.config.spreadsheetId}/values/.*`),
.get( (_uri, request) => this.handleValueUpdate(request as ValueRange)
new RegExp(`/v4/spreadsheets/${this.config.spreadsheetId}/values/.*`) )
)
.reply(200, uri => { // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get
this.get(
new RegExp(`/v4/spreadsheets/${this.config.spreadsheetId}/values/.*`),
uri => {
const range = uri.split("/").pop() const range = uri.split("/").pop()
if (!range) { if (!range) {
throw new Error("No range provided") throw new Error("No range provided")
} }
return this.handleGetValues(decodeURIComponent(range)) return this.handleGetValues(decodeURIComponent(range))
}) }
.persist() )
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append
this.post(
new RegExp(
`/v4/spreadsheets/${this.config.spreadsheetId}/values/.*:append`
),
(_uri, request) => {
const url = new URL(_uri, "https://sheets.googleapis.com/")
const params: Record<string, any> = Object.fromEntries(
url.searchParams.entries()
)
if (params.includeValuesInResponse === "true") {
params.includeValuesInResponse = true
} else {
params.includeValuesInResponse = false
}
const range = url.pathname.split("/").pop()
if (!range) {
throw new Error("No range provided")
}
return this.handleValueAppend({
range,
params,
body: request as ValueRange,
})
}
)
} }
private handleValueAppend(request: AppendRequest): AppendResponse {}
private handleGetValues(range: string): ValueRange { private handleGetValues(range: string): ValueRange {
const { sheet, topLeft, bottomRight } = this.parseA1Notation(range) const { sheet, topLeft, bottomRight } = this.parseA1Notation(range)
const valueRange: ValueRange = { const valueRange: ValueRange = {
@ -415,6 +524,7 @@ export class GoogleSheetsMock {
return cell return cell
} }
// https://developers.google.com/sheets/api/guides/concepts#cell
private parseA1Notation(range: string): { private parseA1Notation(range: string): {
sheet: Sheet sheet: Sheet
topLeft: Range topLeft: Range