chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { createActionWrapper } from '@botpress/common'
|
||||
import { wrapAsyncFnWithTryCatch } from '../google-api/error-handling'
|
||||
import { GoogleClient } from '../google-api/google-client'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export const wrapAction: typeof _wrapAction = (meta, actionImpl) =>
|
||||
_wrapAction(meta, (props) =>
|
||||
wrapAsyncFnWithTryCatch(() => {
|
||||
props.logger
|
||||
.forBot()
|
||||
.debug(`Running action "${meta.actionName}" [bot id: ${props.ctx.botId}]`, { input: props.input })
|
||||
|
||||
return actionImpl(props as Parameters<typeof actionImpl>[0], props.input)
|
||||
}, `Action Error: ${meta.errorMessageWhenFailed}`)()
|
||||
)
|
||||
|
||||
const _wrapAction = createActionWrapper<bp.IntegrationProps>()({
|
||||
toolFactories: {
|
||||
googleClient: GoogleClient.create,
|
||||
},
|
||||
extraMetadata: {} as {
|
||||
errorMessageWhenFailed: string
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const addSheet = wrapAction(
|
||||
{ actionName: 'addSheet', errorMessageWhenFailed: 'Failed to add new sheet' },
|
||||
async ({ googleClient }, { title: sheetTitle }) => await googleClient.createNewSheetInSpreadsheet({ sheetTitle })
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
const _quoteSheetName = (sheetName: string): string => {
|
||||
if (sheetName.startsWith("'") && sheetName.endsWith("'")) {
|
||||
return sheetName
|
||||
}
|
||||
if (/[^A-Za-z0-9_]/.test(sheetName)) {
|
||||
const escaped = sheetName.replace(/'/g, "''")
|
||||
return `'${escaped}'`
|
||||
}
|
||||
return sheetName
|
||||
}
|
||||
|
||||
export const constructRangeFromStartColumn = (sheetName: string | undefined, startColumn: string): string => {
|
||||
const sheetPrefix = sheetName ? `${_quoteSheetName(sheetName)}!` : ''
|
||||
return `${sheetPrefix}${startColumn}:${startColumn}`
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { constructRangeFromStartColumn } from './append-values-utils'
|
||||
|
||||
describe.concurrent('constructRangeFromStartColumn', () => {
|
||||
it('constructs range for simple column without sheet name', () => {
|
||||
expect(constructRangeFromStartColumn(undefined, 'A')).toBe('A:A')
|
||||
expect(constructRangeFromStartColumn(undefined, 'ZZ')).toBe('ZZ:ZZ')
|
||||
})
|
||||
|
||||
it('constructs range for column with sheet name', () => {
|
||||
expect(constructRangeFromStartColumn('Sheet1', 'A')).toBe('Sheet1!A:A')
|
||||
expect(constructRangeFromStartColumn('Sheet1', 'B')).toBe('Sheet1!B:B')
|
||||
expect(constructRangeFromStartColumn('Sheet1', 'AA')).toBe('Sheet1!AA:AA')
|
||||
})
|
||||
|
||||
it('auto-quotes sheet names with spaces', () => {
|
||||
expect(constructRangeFromStartColumn('My Sheet', 'A')).toBe("'My Sheet'!A:A")
|
||||
expect(constructRangeFromStartColumn('Decision Log', 'C')).toBe("'Decision Log'!C:C")
|
||||
})
|
||||
|
||||
it('does not double-quote already quoted sheet names', () => {
|
||||
expect(constructRangeFromStartColumn("'My Sheet'", 'A')).toBe("'My Sheet'!A:A")
|
||||
expect(constructRangeFromStartColumn("'Sheet 1'", 'B')).toBe("'Sheet 1'!B:B")
|
||||
})
|
||||
|
||||
it('quotes sheet names with special characters', () => {
|
||||
expect(constructRangeFromStartColumn('Sheet-1', 'A')).toBe("'Sheet-1'!A:A")
|
||||
expect(constructRangeFromStartColumn('Sheet.1', 'A')).toBe("'Sheet.1'!A:A")
|
||||
})
|
||||
|
||||
it('escapes single quotes (apostrophes) in sheet names', () => {
|
||||
expect(constructRangeFromStartColumn("John's Data", 'A')).toBe("'John''s Data'!A:A")
|
||||
expect(constructRangeFromStartColumn("It's a sheet", 'B')).toBe("'It''s a sheet'!B:B")
|
||||
expect(constructRangeFromStartColumn("A'B'C", 'A')).toBe("'A''B''C'!A:A")
|
||||
})
|
||||
|
||||
it('does not quote simple alphanumeric/underscore sheet names', () => {
|
||||
expect(constructRangeFromStartColumn('Sheet1', 'A')).toBe('Sheet1!A:A')
|
||||
expect(constructRangeFromStartColumn('My_Sheet', 'A')).toBe('My_Sheet!A:A')
|
||||
})
|
||||
|
||||
it('handles multi-character column names', () => {
|
||||
expect(constructRangeFromStartColumn(undefined, 'AB')).toBe('AB:AB')
|
||||
expect(constructRangeFromStartColumn(undefined, 'ABC')).toBe('ABC:ABC')
|
||||
expect(constructRangeFromStartColumn('Sheet1', 'XYZ')).toBe('Sheet1!XYZ:XYZ')
|
||||
})
|
||||
|
||||
it('handles empty string sheet name as undefined', () => {
|
||||
expect(constructRangeFromStartColumn('', 'A')).toBe('A:A')
|
||||
expect(constructRangeFromStartColumn('', 'B')).toBe('B:B')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
import { constructRangeFromStartColumn } from './append-values-utils'
|
||||
|
||||
export const appendValues = wrapAction(
|
||||
{ actionName: 'appendValues', errorMessageWhenFailed: 'Failed to append values into spreadsheet' },
|
||||
async ({ googleClient }, { sheetName, startColumn, values, majorDimension }) =>
|
||||
await googleClient.appendValuesToSpreadsheetRange({
|
||||
rangeA1: constructRangeFromStartColumn(sheetName, startColumn),
|
||||
values,
|
||||
majorDimension,
|
||||
})
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const clearValues = wrapAction(
|
||||
{ actionName: 'clearValues', errorMessageWhenFailed: 'Failed to clear values from the spreadsheet' },
|
||||
async ({ googleClient }, { range: rangeA1 }) => await googleClient.clearValuesFromSpreadsheetRange({ rangeA1 })
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const createNamedRangeInSheet = wrapAction(
|
||||
{ actionName: 'createNamedRangeInSheet', errorMessageWhenFailed: 'Failed to add named range to sheet' },
|
||||
async ({ googleClient }, { rangeName, rangeA1, sheetId }) =>
|
||||
await googleClient.createNamedRangeInSheet({ a1Notation: rangeA1, rangeName, sheetId })
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const deleteRows = wrapAction(
|
||||
{ actionName: 'deleteRows', errorMessageWhenFailed: 'Failed to delete rows' },
|
||||
async ({ googleClient }, { sheetName, rowIndexes }) => {
|
||||
if (rowIndexes.length === 0) {
|
||||
return { deletedCount: 0 }
|
||||
}
|
||||
|
||||
const { sheetId } = await googleClient.getSheetIdByName(sheetName)
|
||||
|
||||
await googleClient.deleteRowsFromSheet({
|
||||
sheetId,
|
||||
rowIndexes,
|
||||
})
|
||||
|
||||
return {
|
||||
deletedCount: rowIndexes.length,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const deleteSheet = wrapAction(
|
||||
{ actionName: 'deleteSheet', errorMessageWhenFailed: 'Failed to delete sheet from spreadsheet' },
|
||||
async ({ googleClient }, { sheetId }) => await googleClient.deleteSheetFromSpreadsheet({ sheetId })
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
import { buildSheetPrefix, columnLetterToIndex } from './row-utils'
|
||||
|
||||
export const findRow = wrapAction(
|
||||
{ actionName: 'findRow', errorMessageWhenFailed: 'Failed to find row' },
|
||||
async ({ googleClient }, { sheetName, searchColumn, searchValue, dataRange }) => {
|
||||
const { sheetTitle } = await googleClient.getSheetIdByName(sheetName)
|
||||
const prefix = buildSheetPrefix(sheetTitle)
|
||||
|
||||
const rangeA1 = dataRange ? `${prefix}${dataRange.split('!').pop()}` : `${prefix}A:ZZ`
|
||||
|
||||
let values: string[][] = []
|
||||
try {
|
||||
const result = await googleClient.getValuesFromSpreadsheetRange({ rangeA1, majorDimension: 'ROWS' })
|
||||
values = result.values ?? []
|
||||
} catch {
|
||||
return { found: false, row: null }
|
||||
}
|
||||
|
||||
if (values.length === 0) {
|
||||
return { found: false, row: null }
|
||||
}
|
||||
|
||||
const searchColumnIndex = columnLetterToIndex(searchColumn)
|
||||
|
||||
for (const [i, row] of values.entries()) {
|
||||
const rowValues = row ?? []
|
||||
const cellValue = rowValues[searchColumnIndex] ?? ''
|
||||
|
||||
if (cellValue === searchValue) {
|
||||
return {
|
||||
found: true,
|
||||
row: {
|
||||
rowIndex: i + 1,
|
||||
values: rowValues.map(String),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { found: false, row: null }
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
import { buildSheetPrefix, columnLetterToIndex } from './row-utils'
|
||||
|
||||
export const findRows = wrapAction(
|
||||
{ actionName: 'findRows', errorMessageWhenFailed: 'Failed to find rows' },
|
||||
async ({ googleClient }, { sheetName, searchColumn, searchValue, dataRange }) => {
|
||||
const { sheetTitle } = await googleClient.getSheetIdByName(sheetName)
|
||||
const prefix = buildSheetPrefix(sheetTitle)
|
||||
|
||||
const rangeA1 = dataRange ? `${prefix}${dataRange.split('!').pop()}` : `${prefix}A:ZZ`
|
||||
|
||||
let values: string[][] = []
|
||||
try {
|
||||
const result = await googleClient.getValuesFromSpreadsheetRange({ rangeA1, majorDimension: 'ROWS' })
|
||||
values = result.values ?? []
|
||||
} catch {
|
||||
return { rows: [], totalMatches: 0 }
|
||||
}
|
||||
|
||||
if (values.length === 0) {
|
||||
return { rows: [], totalMatches: 0 }
|
||||
}
|
||||
|
||||
const searchColumnIndex = columnLetterToIndex(searchColumn)
|
||||
const rows: Array<{ rowIndex: number; values: string[] }> = []
|
||||
|
||||
for (const [i, row] of values.entries()) {
|
||||
const rowValues = row ?? []
|
||||
const cellValue = rowValues[searchColumnIndex] ?? ''
|
||||
|
||||
if (cellValue === searchValue) {
|
||||
rows.push({
|
||||
rowIndex: i + 1,
|
||||
values: rowValues.map(String),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rows,
|
||||
totalMatches: rows.length,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const getAllSheetsInSpreadsheet = wrapAction(
|
||||
{ actionName: 'getAllSheetsInSpreadsheet', errorMessageWhenFailed: 'Failed to obtain sheets' },
|
||||
async ({ googleClient }) => ({
|
||||
sheets: await googleClient.getAllSheetsInSpreadsheet(),
|
||||
})
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const getInfoSpreadsheet = wrapAction(
|
||||
{ actionName: 'getInfoSpreadsheet', errorMessageWhenFailed: 'Failed to get spreadsheet info' },
|
||||
async ({ googleClient }, { fields }) => await googleClient.getSpreadsheetMetadata({ fields: fields.join(',') })
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const getNamedRanges = wrapAction(
|
||||
{ actionName: 'getNamedRanges', errorMessageWhenFailed: 'Failed to obtain the named ranges of the spreadsheet' },
|
||||
async ({ googleClient }) => ({
|
||||
namedRanges: await googleClient.getNamedRanges(),
|
||||
})
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const getProtectedRanges = wrapAction(
|
||||
{
|
||||
actionName: 'getProtectedRanges',
|
||||
errorMessageWhenFailed: 'Failed to obtain the protected ranges of the spreadsheet',
|
||||
},
|
||||
async ({ googleClient }) => ({
|
||||
protectedRanges: await googleClient.getProtectedRanges(),
|
||||
})
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
import { buildRowRange } from './row-utils'
|
||||
|
||||
export const getRow = wrapAction(
|
||||
{ actionName: 'getRow', errorMessageWhenFailed: 'Failed to get row' },
|
||||
async ({ googleClient }, { sheetName, rowIndex, startColumn, endColumn }) => {
|
||||
const { sheetTitle } = await googleClient.getSheetIdByName(sheetName)
|
||||
|
||||
const rangeA1 = buildRowRange({
|
||||
sheetTitle,
|
||||
rowIndex,
|
||||
startColumn: startColumn ?? 'A',
|
||||
endColumn,
|
||||
})
|
||||
|
||||
let values: string[][] = []
|
||||
try {
|
||||
const result = await googleClient.getValuesFromSpreadsheetRange({ rangeA1, majorDimension: 'ROWS' })
|
||||
values = result.values ?? []
|
||||
} catch {
|
||||
return { found: false, row: null }
|
||||
}
|
||||
|
||||
if (values.length === 0 || !values[0] || values[0].length === 0) {
|
||||
return { found: false, row: null }
|
||||
}
|
||||
|
||||
return {
|
||||
found: true,
|
||||
row: {
|
||||
rowIndex,
|
||||
values: values[0].map(String),
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const getValues = wrapAction(
|
||||
{ actionName: 'getValues', errorMessageWhenFailed: 'Failed to get values from the specified range' },
|
||||
async ({ googleClient }, { range: rangeA1, majorDimension }) =>
|
||||
await googleClient.getValuesFromSpreadsheetRange({
|
||||
rangeA1,
|
||||
majorDimension,
|
||||
})
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
import { buildRowRange, indexToColumnLetter, columnLetterToIndex } from './row-utils'
|
||||
|
||||
export const insertRowAtIndex = wrapAction(
|
||||
{ actionName: 'insertRowAtIndex', errorMessageWhenFailed: 'Failed to insert row at index' },
|
||||
async ({ googleClient }, { sheetName, rowIndex, values, startColumn }) => {
|
||||
const { sheetId, sheetTitle } = await googleClient.getSheetIdByName(sheetName)
|
||||
|
||||
await googleClient.insertRows({
|
||||
sheetId,
|
||||
startIndex: rowIndex - 1,
|
||||
numberOfRows: 1,
|
||||
})
|
||||
|
||||
if (values && values.length > 0) {
|
||||
const start = startColumn ?? 'A'
|
||||
const startColIndex = columnLetterToIndex(start)
|
||||
const endColIndex = startColIndex + values.length - 1
|
||||
const endColumn = indexToColumnLetter(endColIndex)
|
||||
|
||||
const rangeA1 = buildRowRange({
|
||||
sheetTitle,
|
||||
rowIndex,
|
||||
startColumn: start,
|
||||
endColumn,
|
||||
})
|
||||
|
||||
await googleClient.updateValuesInSpreadsheetRange({
|
||||
rangeA1,
|
||||
values: [values],
|
||||
majorDimension: 'ROWS',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
insertedRowIndex: rowIndex,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const moveSheetHorizontally = wrapAction(
|
||||
{ actionName: 'moveSheetHorizontally', errorMessageWhenFailed: 'Failed to move sheet to the new index' },
|
||||
async ({ googleClient }, { sheetId, newIndex }) => await googleClient.moveSheetToIndex({ sheetId, newIndex })
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const protectNamedRange = wrapAction(
|
||||
{ actionName: 'protectNamedRange', errorMessageWhenFailed: 'Failed to protect the named range' },
|
||||
async ({ googleClient }, { namedRangeId, requestingUserCanEdit, warningOnly }) =>
|
||||
await googleClient.protectNamedRange({ namedRangeId, requestingUserCanEdit, warningOnly })
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const renameSheet = wrapAction(
|
||||
{ actionName: 'renameSheet', errorMessageWhenFailed: 'Failed to rename sheet' },
|
||||
async ({ googleClient }, { sheetId, newTitle }) => await googleClient.renameSheetInSpreadsheet({ sheetId, newTitle })
|
||||
)
|
||||
@@ -0,0 +1,129 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
columnLetterToIndex,
|
||||
indexToColumnLetter,
|
||||
buildSheetPrefix,
|
||||
buildRowRange,
|
||||
buildColumnRange,
|
||||
} from './row-utils'
|
||||
|
||||
describe.concurrent('columnLetterToIndex', () => {
|
||||
it('converts single letters A-Z to indices 0-25', () => {
|
||||
expect(columnLetterToIndex('A')).toBe(0)
|
||||
expect(columnLetterToIndex('B')).toBe(1)
|
||||
expect(columnLetterToIndex('Z')).toBe(25)
|
||||
})
|
||||
|
||||
it('converts double letters starting with AA', () => {
|
||||
expect(columnLetterToIndex('AA')).toBe(26)
|
||||
expect(columnLetterToIndex('AB')).toBe(27)
|
||||
expect(columnLetterToIndex('AZ')).toBe(51)
|
||||
})
|
||||
|
||||
it('converts BA and beyond', () => {
|
||||
expect(columnLetterToIndex('BA')).toBe(52)
|
||||
expect(columnLetterToIndex('ZZ')).toBe(701)
|
||||
})
|
||||
|
||||
it('is case-insensitive', () => {
|
||||
expect(columnLetterToIndex('a')).toBe(0)
|
||||
expect(columnLetterToIndex('aa')).toBe(26)
|
||||
expect(columnLetterToIndex('Aa')).toBe(26)
|
||||
})
|
||||
})
|
||||
|
||||
describe.concurrent('indexToColumnLetter', () => {
|
||||
it('converts indices 0-25 to single letters A-Z', () => {
|
||||
expect(indexToColumnLetter(0)).toBe('A')
|
||||
expect(indexToColumnLetter(1)).toBe('B')
|
||||
expect(indexToColumnLetter(25)).toBe('Z')
|
||||
})
|
||||
|
||||
it('converts index 26+ to double letters', () => {
|
||||
expect(indexToColumnLetter(26)).toBe('AA')
|
||||
expect(indexToColumnLetter(27)).toBe('AB')
|
||||
expect(indexToColumnLetter(51)).toBe('AZ')
|
||||
})
|
||||
|
||||
it('converts higher indices correctly', () => {
|
||||
expect(indexToColumnLetter(52)).toBe('BA')
|
||||
expect(indexToColumnLetter(701)).toBe('ZZ')
|
||||
})
|
||||
})
|
||||
|
||||
describe.concurrent('columnLetterToIndex and indexToColumnLetter roundtrip', () => {
|
||||
it('indexToColumnLetter inverts columnLetterToIndex', () => {
|
||||
const letters = ['A', 'Z', 'AA', 'AZ', 'BA', 'ZZ']
|
||||
for (const letter of letters) {
|
||||
expect(indexToColumnLetter(columnLetterToIndex(letter))).toBe(letter)
|
||||
}
|
||||
})
|
||||
|
||||
it('columnLetterToIndex inverts indexToColumnLetter', () => {
|
||||
const indices = [0, 25, 26, 51, 52, 701]
|
||||
for (const index of indices) {
|
||||
expect(columnLetterToIndex(indexToColumnLetter(index))).toBe(index)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe.concurrent('buildSheetPrefix', () => {
|
||||
it('returns empty string when no sheet title', () => {
|
||||
expect(buildSheetPrefix()).toBe('')
|
||||
expect(buildSheetPrefix(undefined)).toBe('')
|
||||
})
|
||||
|
||||
it('returns simple prefix for alphanumeric titles', () => {
|
||||
expect(buildSheetPrefix('Sheet1')).toBe('Sheet1!')
|
||||
expect(buildSheetPrefix('Data')).toBe('Data!')
|
||||
})
|
||||
|
||||
it('quotes titles containing spaces', () => {
|
||||
expect(buildSheetPrefix('My Sheet')).toBe("'My Sheet'!")
|
||||
})
|
||||
|
||||
it('quotes and escapes titles containing apostrophes', () => {
|
||||
expect(buildSheetPrefix("It's")).toBe("'It''s'!")
|
||||
expect(buildSheetPrefix("Don't stop")).toBe("'Don''t stop'!")
|
||||
})
|
||||
})
|
||||
|
||||
describe.concurrent('buildRowRange', () => {
|
||||
it('builds simple row range with defaults', () => {
|
||||
expect(buildRowRange({ rowIndex: 1 })).toBe('A1:1')
|
||||
})
|
||||
|
||||
it('builds row range with explicit start and end columns', () => {
|
||||
expect(buildRowRange({ rowIndex: 5, startColumn: 'B', endColumn: 'D' })).toBe('B5:D5')
|
||||
})
|
||||
|
||||
it('includes sheet prefix when provided', () => {
|
||||
expect(buildRowRange({ sheetTitle: 'Data', rowIndex: 3, startColumn: 'A', endColumn: 'C' })).toBe('Data!A3:C3')
|
||||
})
|
||||
|
||||
it('handles sheet titles requiring quotes', () => {
|
||||
expect(buildRowRange({ sheetTitle: 'My Data', rowIndex: 1, endColumn: 'B' })).toBe("'My Data'!A1:B1")
|
||||
})
|
||||
})
|
||||
|
||||
describe.concurrent('buildColumnRange', () => {
|
||||
it('builds single column range with defaults', () => {
|
||||
expect(buildColumnRange({})).toBe('A1:A100000')
|
||||
})
|
||||
|
||||
it('builds range for specified columns', () => {
|
||||
expect(buildColumnRange({ startColumn: 'B', endColumn: 'D' })).toBe('B1:D100000')
|
||||
})
|
||||
|
||||
it('uses startColumn as endColumn when endColumn not specified', () => {
|
||||
expect(buildColumnRange({ startColumn: 'C' })).toBe('C1:C100000')
|
||||
})
|
||||
|
||||
it('respects custom maxRow', () => {
|
||||
expect(buildColumnRange({ startColumn: 'A', endColumn: 'B', maxRow: 500 })).toBe('A1:B500')
|
||||
})
|
||||
|
||||
it('includes sheet prefix when provided', () => {
|
||||
expect(buildColumnRange({ sheetTitle: 'Sales', startColumn: 'A', endColumn: 'C' })).toBe('Sales!A1:C100000')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
const ASCII_UPPERCASE_A = 65
|
||||
const BASE_26 = 26
|
||||
|
||||
export const columnLetterToIndex = (col: string): number =>
|
||||
col
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.reduce((acc, char) => acc * BASE_26 + char.charCodeAt(0) - ASCII_UPPERCASE_A + 1, 0) - 1
|
||||
|
||||
export const indexToColumnLetter = (index: number): string => {
|
||||
let letter = ''
|
||||
let i = index
|
||||
while (i >= 0) {
|
||||
letter = String.fromCharCode((i % BASE_26) + ASCII_UPPERCASE_A) + letter
|
||||
i = Math.floor(i / BASE_26) - 1
|
||||
}
|
||||
return letter
|
||||
}
|
||||
|
||||
export const buildSheetPrefix = (sheetTitle?: string): string => {
|
||||
if (!sheetTitle) {
|
||||
return ''
|
||||
}
|
||||
const needsQuotes = sheetTitle.includes(' ') || sheetTitle.includes("'")
|
||||
return needsQuotes ? `'${sheetTitle.replace(/'/g, "''")}'!` : `${sheetTitle}!`
|
||||
}
|
||||
|
||||
export const buildRowRange = ({
|
||||
sheetTitle,
|
||||
rowIndex,
|
||||
startColumn = 'A',
|
||||
endColumn,
|
||||
}: {
|
||||
sheetTitle?: string
|
||||
rowIndex: number
|
||||
startColumn?: string
|
||||
endColumn?: string
|
||||
}): string => {
|
||||
const prefix = buildSheetPrefix(sheetTitle)
|
||||
const end = endColumn ? `${endColumn}${rowIndex}` : `${rowIndex}`
|
||||
return `${prefix}${startColumn}${rowIndex}:${end}`
|
||||
}
|
||||
|
||||
export const buildColumnRange = ({
|
||||
sheetTitle,
|
||||
startColumn = 'A',
|
||||
endColumn,
|
||||
maxRow = 100000,
|
||||
}: {
|
||||
sheetTitle?: string
|
||||
startColumn?: string
|
||||
endColumn?: string
|
||||
maxRow?: number
|
||||
}): string => {
|
||||
const prefix = buildSheetPrefix(sheetTitle)
|
||||
const end = endColumn ?? startColumn
|
||||
return `${prefix}${startColumn}1:${end}${maxRow}`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const setSheetVisibility = wrapAction(
|
||||
{ actionName: 'setSheetVisibility', errorMessageWhenFailed: 'Failed to set sheet visibility' },
|
||||
async ({ googleClient }, { sheetId, isHidden }) => await googleClient.setSheetVisibility({ sheetId, isHidden })
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const setValues = wrapAction(
|
||||
{ actionName: 'setValues', errorMessageWhenFailed: 'Failed to set values in the specified range' },
|
||||
async ({ googleClient }, { range: rangeA1, majorDimension, values }) =>
|
||||
await googleClient.updateValuesInSpreadsheetRange({
|
||||
rangeA1,
|
||||
majorDimension,
|
||||
values,
|
||||
})
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
|
||||
export const unprotectRange = wrapAction(
|
||||
{ actionName: 'unprotectRange', errorMessageWhenFailed: 'Failed to delete protected range' },
|
||||
async ({ googleClient }, { protectedRangeId }) => await googleClient.unprotectRange({ protectedRangeId })
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
import { buildRowRange, indexToColumnLetter, columnLetterToIndex } from './row-utils'
|
||||
|
||||
export const updateRow = wrapAction(
|
||||
{ actionName: 'updateRow', errorMessageWhenFailed: 'Failed to update row' },
|
||||
async ({ googleClient }, { sheetName, rowIndex, values, startColumn }) => {
|
||||
const { sheetTitle } = await googleClient.getSheetIdByName(sheetName)
|
||||
|
||||
const start = startColumn ?? 'A'
|
||||
const startColIndex = columnLetterToIndex(start)
|
||||
const endColIndex = startColIndex + values.length - 1
|
||||
const endColumn = indexToColumnLetter(endColIndex)
|
||||
|
||||
const rangeA1 = buildRowRange({
|
||||
sheetTitle,
|
||||
rowIndex,
|
||||
startColumn: start,
|
||||
endColumn,
|
||||
})
|
||||
|
||||
const result = await googleClient.updateValuesInSpreadsheetRange({
|
||||
rangeA1,
|
||||
values: [values],
|
||||
majorDimension: 'ROWS',
|
||||
})
|
||||
|
||||
return {
|
||||
updatedRange: result.updatedRange,
|
||||
updatedCells: result.updatedCells,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
import { wrapAction } from '../action-wrapper'
|
||||
import { buildSheetPrefix, buildRowRange, columnLetterToIndex, indexToColumnLetter } from './row-utils'
|
||||
|
||||
export const upsertRow = wrapAction(
|
||||
{ actionName: 'upsertRow', errorMessageWhenFailed: 'Failed to upsert row' },
|
||||
async ({ googleClient }, { sheetName, keyColumn, keyValue, values, startColumn }) => {
|
||||
const { sheetTitle } = await googleClient.getSheetIdByName(sheetName)
|
||||
const prefix = buildSheetPrefix(sheetTitle)
|
||||
|
||||
const rangeA1 = `${prefix}A:ZZ`
|
||||
|
||||
let existingValues: string[][] = []
|
||||
try {
|
||||
const result = await googleClient.getValuesFromSpreadsheetRange({ rangeA1, majorDimension: 'ROWS' })
|
||||
existingValues = result.values ?? []
|
||||
} catch {
|
||||
existingValues = []
|
||||
}
|
||||
|
||||
const keyColumnIndex = columnLetterToIndex(keyColumn)
|
||||
let matchingRowIndex: number | null = null
|
||||
|
||||
for (const [i, row] of existingValues.entries()) {
|
||||
const rowValues = row ?? []
|
||||
const cellValue = rowValues[keyColumnIndex] ?? ''
|
||||
|
||||
if (cellValue === keyValue) {
|
||||
matchingRowIndex = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const start = startColumn ?? 'A'
|
||||
const startColIndex = columnLetterToIndex(start)
|
||||
const endColIndex = startColIndex + values.length - 1
|
||||
const endColumn = indexToColumnLetter(endColIndex)
|
||||
|
||||
if (matchingRowIndex !== null) {
|
||||
const updateRangeA1 = buildRowRange({
|
||||
sheetTitle,
|
||||
rowIndex: matchingRowIndex,
|
||||
startColumn: start,
|
||||
endColumn,
|
||||
})
|
||||
|
||||
await googleClient.updateValuesInSpreadsheetRange({
|
||||
rangeA1: updateRangeA1,
|
||||
values: [values],
|
||||
majorDimension: 'ROWS',
|
||||
})
|
||||
|
||||
return {
|
||||
action: 'updated' as const,
|
||||
rowIndex: matchingRowIndex,
|
||||
}
|
||||
}
|
||||
|
||||
const appendRangeA1 = `${prefix}${start}:${endColumn}`
|
||||
|
||||
const appendResult = await googleClient.appendValuesToSpreadsheetRange({
|
||||
rangeA1: appendRangeA1,
|
||||
values: [values],
|
||||
majorDimension: 'ROWS',
|
||||
})
|
||||
|
||||
const updatedRange = appendResult.updates.updatedRange
|
||||
const rowMatch = updatedRange.match(/:?[A-Z]+(\d+)$/)
|
||||
const insertedRowIndex = rowMatch?.[1] ? parseInt(rowMatch[1], 10) : existingValues.length + 1
|
||||
|
||||
return {
|
||||
action: 'inserted' as const,
|
||||
rowIndex: insertedRowIndex,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
import { addSheet } from './implementations/add-sheet'
|
||||
import { appendValues } from './implementations/append-values'
|
||||
import { clearValues } from './implementations/clear-values'
|
||||
import { createNamedRangeInSheet } from './implementations/create-named-range-in-sheet'
|
||||
import { deleteRows } from './implementations/delete-rows'
|
||||
import { deleteSheet } from './implementations/delete-sheet'
|
||||
import { findRow } from './implementations/find-row'
|
||||
import { findRows } from './implementations/find-rows'
|
||||
import { getAllSheetsInSpreadsheet } from './implementations/get-all-sheets-in-spreadsheet'
|
||||
import { getInfoSpreadsheet } from './implementations/get-info-spread-sheet'
|
||||
import { getNamedRanges } from './implementations/get-named-ranges'
|
||||
import { getProtectedRanges } from './implementations/get-protected-ranges'
|
||||
import { getRow } from './implementations/get-row'
|
||||
import { getValues } from './implementations/get-values'
|
||||
import { insertRowAtIndex } from './implementations/insert-row-at-index'
|
||||
import { moveSheetHorizontally } from './implementations/move-sheet-horizontally'
|
||||
import { protectNamedRange } from './implementations/protect-named-range'
|
||||
import { renameSheet } from './implementations/rename-sheet'
|
||||
import { setSheetVisibility } from './implementations/set-sheet-visibility'
|
||||
import { setValues } from './implementations/set-values'
|
||||
import { unprotectRange } from './implementations/unprotect-range'
|
||||
import { updateRow } from './implementations/update-row'
|
||||
import { upsertRow } from './implementations/upsert-row'
|
||||
import * as bp from '.botpress'
|
||||
|
||||
export default {
|
||||
addSheet,
|
||||
appendValues,
|
||||
clearValues,
|
||||
createNamedRangeInSheet,
|
||||
deleteRows,
|
||||
deleteSheet,
|
||||
findRow,
|
||||
findRows,
|
||||
getAllSheetsInSpreadsheet,
|
||||
getInfoSpreadsheet,
|
||||
getNamedRanges,
|
||||
getProtectedRanges,
|
||||
getRow,
|
||||
getValues,
|
||||
insertRowAtIndex,
|
||||
moveSheetHorizontally,
|
||||
protectNamedRange,
|
||||
renameSheet,
|
||||
setSheetVisibility,
|
||||
setValues,
|
||||
unprotectRange,
|
||||
updateRow,
|
||||
upsertRow,
|
||||
} as const satisfies bp.IntegrationProps['actions']
|
||||
Reference in New Issue
Block a user