First new test passing.
This commit is contained in:
parent
3c58a593f9
commit
0359b20347
|
@ -53,6 +53,7 @@ export async function save(
|
|||
builderSocket?.emitDatasourceUpdate(ctx, datasource)
|
||||
return table
|
||||
} catch (err: any) {
|
||||
throw err
|
||||
if (err instanceof Error) {
|
||||
ctx.throw(400, err.message)
|
||||
} else {
|
||||
|
|
|
@ -49,17 +49,11 @@ describe("Google Sheets Integration", () => {
|
|||
beforeEach(async () => {
|
||||
nock.cleanAll()
|
||||
mock = GoogleSheetsMock.forDatasource(datasource)
|
||||
mock.mockAuth()
|
||||
mock.init()
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("creates a new table", async () => {
|
||||
nock("https://sheets.googleapis.com/", {
|
||||
reqheaders: { authorization: "Bearer test" },
|
||||
})
|
||||
.get("/v4/spreadsheets/randomId/")
|
||||
.reply(200, {})
|
||||
|
||||
const table = await config.api.table.save({
|
||||
name: "Test Table",
|
||||
type: "table",
|
||||
|
@ -82,6 +76,12 @@ describe("Google Sheets Integration", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
|
||||
const cell = mock.getCell(table.name, "A1")
|
||||
if (!cell) {
|
||||
throw new Error("Cell not found")
|
||||
}
|
||||
expect(cell.userEnteredValue.stringValue).toEqual(table.name)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2,6 +2,82 @@ import { Datasource } from "@budibase/types"
|
|||
import nock from "nock"
|
||||
import { GoogleSheetsConfig } from "../../googlesheets"
|
||||
|
||||
type Value = string | number | boolean
|
||||
type Dimension = "ROWS" | "COLUMNS"
|
||||
|
||||
interface Range {
|
||||
row: number | "ALL"
|
||||
column: number | "ALL"
|
||||
}
|
||||
|
||||
interface DimensionProperties {
|
||||
hiddenByFilter: boolean
|
||||
hiddenByUser: boolean
|
||||
pixelSize: number
|
||||
// developerMetadata: DeveloperMetadata[]
|
||||
// dataSourceColumnReference: DataSourceColumnReference
|
||||
}
|
||||
|
||||
interface ValueRange {
|
||||
range: string
|
||||
majorDimension: Dimension
|
||||
values: Value[][]
|
||||
}
|
||||
|
||||
interface UpdateValuesResponse {
|
||||
spreadsheetId: string
|
||||
updatedRange: string
|
||||
updatedRows: number
|
||||
updatedColumns: number
|
||||
updatedCells: number
|
||||
updatedData: ValueRange
|
||||
}
|
||||
|
||||
interface AddSheetResponse {
|
||||
properties: SheetProperties
|
||||
}
|
||||
|
||||
interface Response {
|
||||
addSheet?: AddSheetResponse
|
||||
}
|
||||
|
||||
interface BatchUpdateResponse {
|
||||
spreadsheetId: string
|
||||
replies: Response[]
|
||||
updatedSpreadsheet: Spreadsheet
|
||||
}
|
||||
|
||||
interface GridProperties {
|
||||
rowCount: number
|
||||
columnCount: number
|
||||
frozenRowCount: number
|
||||
frozenColumnCount: number
|
||||
hideGridlines: boolean
|
||||
rowGroupControlAfter: boolean
|
||||
columnGroupControlAfter: boolean
|
||||
}
|
||||
|
||||
interface SheetProperties {
|
||||
sheetId: number
|
||||
title: string
|
||||
gridProperties: GridProperties
|
||||
}
|
||||
|
||||
interface AddSheetRequest {
|
||||
properties: SheetProperties
|
||||
}
|
||||
|
||||
interface Request {
|
||||
addSheet?: AddSheetRequest
|
||||
}
|
||||
|
||||
interface BatchUpdateRequest {
|
||||
requests: Request[]
|
||||
includeSpreadsheetInResponse: boolean
|
||||
responseRanges: string[]
|
||||
responseIncludeGridData: boolean
|
||||
}
|
||||
|
||||
interface ErrorValue {
|
||||
type: string
|
||||
message: string
|
||||
|
@ -27,17 +103,21 @@ interface GridData {
|
|||
startRow: number
|
||||
startColumn: number
|
||||
rowData: RowData[]
|
||||
rowMetadata: DimensionProperties[]
|
||||
columnMetadata: DimensionProperties[]
|
||||
}
|
||||
|
||||
interface Sheet {
|
||||
properties: {
|
||||
sheetId: string
|
||||
title: string
|
||||
}
|
||||
properties: SheetProperties
|
||||
data: GridData[]
|
||||
}
|
||||
|
||||
interface SpreadsheetProperties {
|
||||
title: string
|
||||
}
|
||||
|
||||
interface Spreadsheet {
|
||||
properties: SpreadsheetProperties
|
||||
spreadsheetId: string
|
||||
sheets: Sheet[]
|
||||
}
|
||||
|
@ -53,6 +133,9 @@ export class GoogleSheetsMock {
|
|||
private constructor(config: GoogleSheetsConfig) {
|
||||
this.config = config
|
||||
this.sheet = {
|
||||
properties: {
|
||||
title: "Test Spreadsheet",
|
||||
},
|
||||
spreadsheetId: config.spreadsheetId,
|
||||
sheets: [],
|
||||
}
|
||||
|
@ -82,12 +165,250 @@ export class GoogleSheetsMock {
|
|||
reqheaders: { authorization: "Bearer test" },
|
||||
})
|
||||
.get("/v4/spreadsheets/randomId/")
|
||||
.reply(200, {})
|
||||
.reply(200, () => this.sheet)
|
||||
.persist()
|
||||
|
||||
nock("https://sheets.googleapis.com/", {
|
||||
reqheaders: { authorization: "Bearer test" },
|
||||
})
|
||||
.post(`/v4/spreadsheets/${this.config.spreadsheetId}/:batchUpdate`)
|
||||
.reply(200, () => {})
|
||||
.reply(200, (uri: string, request: nock.Body): nock.Body => {
|
||||
const batchUpdateRequest = request as BatchUpdateRequest
|
||||
const replies: Response[] = []
|
||||
|
||||
for (const request of batchUpdateRequest.requests) {
|
||||
if (request.addSheet) {
|
||||
const properties: SheetProperties = {
|
||||
title: request.addSheet.properties.title,
|
||||
sheetId: this.sheet.sheets.length,
|
||||
gridProperties: {
|
||||
rowCount: 100,
|
||||
columnCount: 26,
|
||||
frozenRowCount: 0,
|
||||
frozenColumnCount: 0,
|
||||
hideGridlines: false,
|
||||
rowGroupControlAfter: false,
|
||||
columnGroupControlAfter: false,
|
||||
},
|
||||
}
|
||||
|
||||
this.sheet.sheets.push({
|
||||
properties,
|
||||
data: [this.createEmptyGrid(100, 26)],
|
||||
})
|
||||
|
||||
replies.push({ addSheet: { properties } })
|
||||
}
|
||||
}
|
||||
|
||||
const response: BatchUpdateResponse = {
|
||||
spreadsheetId: this.sheet.spreadsheetId,
|
||||
replies,
|
||||
updatedSpreadsheet: this.sheet,
|
||||
}
|
||||
return response
|
||||
})
|
||||
.persist()
|
||||
|
||||
nock("https://sheets.googleapis.com/", {
|
||||
reqheaders: { authorization: "Bearer test" },
|
||||
})
|
||||
.put(
|
||||
new RegExp(`/v4/spreadsheets/${this.config.spreadsheetId}/values/.*`)
|
||||
)
|
||||
.reply(200, (uri, request) =>
|
||||
this.handleValueUpdate(request as ValueRange)
|
||||
)
|
||||
}
|
||||
|
||||
private handleValueUpdate(valueRange: ValueRange): UpdateValuesResponse {
|
||||
if (valueRange.majorDimension !== "ROWS") {
|
||||
throw new Error("Only row-major updates are supported")
|
||||
}
|
||||
|
||||
const { sheet, topLeft, bottomRight } = this.parseA1Notation(
|
||||
valueRange.range
|
||||
)
|
||||
|
||||
if (topLeft.row === "ALL") {
|
||||
topLeft.row = 0
|
||||
}
|
||||
if (bottomRight.row === "ALL") {
|
||||
bottomRight.row = sheet.properties.gridProperties.rowCount - 1
|
||||
}
|
||||
if (topLeft.column === "ALL") {
|
||||
topLeft.column = 0
|
||||
}
|
||||
if (bottomRight.column === "ALL") {
|
||||
bottomRight.column = sheet.properties.gridProperties.columnCount - 1
|
||||
}
|
||||
|
||||
for (let row = topLeft.row; row <= bottomRight.row; row++) {
|
||||
for (
|
||||
let column = topLeft.column;
|
||||
column <= bottomRight.column;
|
||||
column++
|
||||
) {
|
||||
const cell = this.getCellNumericIndexes(sheet, row, column)
|
||||
if (!cell) {
|
||||
continue
|
||||
}
|
||||
const value =
|
||||
valueRange.values[row - topLeft.row][column - topLeft.column]
|
||||
cell.userEnteredValue = this.createValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
const response: UpdateValuesResponse = {
|
||||
spreadsheetId: this.sheet.spreadsheetId,
|
||||
updatedRange: valueRange.range,
|
||||
updatedRows: valueRange.values.length,
|
||||
updatedColumns: valueRange.values[0].length,
|
||||
updatedCells: valueRange.values.length * valueRange.values[0].length,
|
||||
updatedData: valueRange,
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
private createValue(from: Value): ExtendedValue {
|
||||
if (typeof from === "string") {
|
||||
return {
|
||||
stringValue: from,
|
||||
}
|
||||
} else if (typeof from === "number") {
|
||||
return {
|
||||
numberValue: from,
|
||||
}
|
||||
} else if (typeof from === "boolean") {
|
||||
return {
|
||||
boolValue: from,
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unsupported value type")
|
||||
}
|
||||
}
|
||||
|
||||
private createEmptyGrid(numRows: number, numCols: number): GridData {
|
||||
const rowData: RowData[] = []
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
const cells: CellData[] = []
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
cells.push({
|
||||
userEnteredValue: {
|
||||
stringValue: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
rowData.push({
|
||||
values: cells,
|
||||
})
|
||||
}
|
||||
const rowMetadata: DimensionProperties[] = []
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
rowMetadata.push({
|
||||
hiddenByFilter: false,
|
||||
hiddenByUser: false,
|
||||
pixelSize: 100,
|
||||
})
|
||||
}
|
||||
const columnMetadata: DimensionProperties[] = []
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
columnMetadata.push({
|
||||
hiddenByFilter: false,
|
||||
hiddenByUser: false,
|
||||
pixelSize: 100,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
rowData,
|
||||
rowMetadata,
|
||||
columnMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
getCell(sheetName: string, ref: string): CellData | undefined {
|
||||
const sheet = this.getSheetByName(sheetName)
|
||||
if (!sheet) {
|
||||
return undefined
|
||||
}
|
||||
const { row, column } = this.parseCell(ref)
|
||||
if (row === "ALL" || column === "ALL") {
|
||||
throw new Error("Only single cell references are supported")
|
||||
}
|
||||
return this.getCellNumericIndexes(sheet, row, column)
|
||||
}
|
||||
|
||||
private getCellNumericIndexes(
|
||||
sheet: Sheet,
|
||||
row: number,
|
||||
column: number
|
||||
): CellData | undefined {
|
||||
const data = sheet.data[0]
|
||||
const rowData = data.rowData[row]
|
||||
if (!rowData) {
|
||||
return undefined
|
||||
}
|
||||
const cell = rowData.values[column]
|
||||
if (!cell) {
|
||||
return undefined
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
private parseA1Notation(range: string): {
|
||||
sheet: Sheet
|
||||
topLeft: Range
|
||||
bottomRight: Range
|
||||
} {
|
||||
let [sheetName, rest] = range.split("!")
|
||||
const [topLeft, bottomRight] = rest.split(":")
|
||||
|
||||
if (sheetName.startsWith("'") && sheetName.endsWith("'")) {
|
||||
sheetName = sheetName.slice(1, -1)
|
||||
}
|
||||
|
||||
const sheet = this.getSheetByName(sheetName)
|
||||
if (!sheet) {
|
||||
throw new Error(`Sheet ${sheetName} not found`)
|
||||
}
|
||||
|
||||
return {
|
||||
sheet,
|
||||
topLeft: this.parseCell(topLeft),
|
||||
bottomRight: this.parseCell(bottomRight),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a cell reference into a row and column.
|
||||
* @param cell a string of the form A1, B2, etc.
|
||||
* @returns
|
||||
*/
|
||||
private parseCell(cell: string): Range {
|
||||
const firstChar = cell.slice(0, 1)
|
||||
if (this.isInteger(firstChar)) {
|
||||
return { row: parseInt(cell) - 1, column: "ALL" }
|
||||
}
|
||||
const column = this.letterToNumber(firstChar)
|
||||
if (cell.length === 1) {
|
||||
return { row: "ALL", column }
|
||||
}
|
||||
const number = cell.slice(1)
|
||||
return { row: parseInt(number) - 1, column }
|
||||
}
|
||||
|
||||
private isInteger(value: string): boolean {
|
||||
return !isNaN(parseInt(value))
|
||||
}
|
||||
|
||||
private letterToNumber(letter: string): number {
|
||||
return letter.charCodeAt(0) - 65
|
||||
}
|
||||
|
||||
private getSheetByName(name: string): Sheet | undefined {
|
||||
return this.sheet.sheets.find(sheet => sheet.properties.title === name)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue