feat(google-sheets): add row filtering to read with numeric operators (#4822)

* feat(google-sheets): add row filtering to read with numeric operators

Adds client-side row filtering to the Google Sheets read (v2) operation.
Filter the returned rows by a header column using text operators
(contains, not_contains, exact, not_equals, starts_with, ends_with) and
numeric/ordering operators (gt, gte, lt, lte). Filtering lives in a pure,
unit-tested helper (filterSheetRows) and runs over the fetched read range;
an optional `filter` output reports whether the column was found and how
many rows matched.

Also hardens the surrounding tools:
- trim spreadsheetId in write/update/append URL builders (matches read)
- URL-encode the v1 read default range
- expose valueInputOption for the update operation in the block

Backwards compatible: with no filter requested, read output is byte-
identical and the `filter` field is omitted. The filterMatchType union is
widened additively (4 -> 10 values).

* fix(google-sheets): correct filter metadata for missing column and header-only sheets

- matchedRows is now 0 (not totalRows) when the filter column is not found,
  so it no longer contradicts applied=false / columnFound=false
- columnFound now reflects an actual header lookup for empty/header-only
  sheets instead of being hardcoded true
- add tests covering header-only and empty sheets with present/absent columns
This commit is contained in:
Waleed
2026-05-30 20:09:53 -07:00
committed by GitHub
parent 6c476cf592
commit f6685cf1b1
9 changed files with 548 additions and 44 deletions
+19 -3
View File
@@ -462,9 +462,15 @@ Return ONLY the range string - no sheet name, no explanations, no quotes.`,
type: 'dropdown',
options: [
{ label: 'Contains', id: 'contains' },
{ label: 'Does Not Contain', id: 'not_contains' },
{ label: 'Exact Match', id: 'exact' },
{ label: 'Not Equal To', id: 'not_equals' },
{ label: 'Starts With', id: 'starts_with' },
{ label: 'Ends With', id: 'ends_with' },
{ label: 'Greater Than', id: 'gt' },
{ label: 'Greater Than or Equal', id: 'gte' },
{ label: 'Less Than', id: 'lt' },
{ label: 'Less Than or Equal', id: 'lte' },
],
condition: { field: 'operation', value: 'read' },
mode: 'advanced',
@@ -503,7 +509,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
{ label: 'User Entered (Parse formulas)', id: 'USER_ENTERED' },
{ label: "Raw (Don't parse formulas)", id: 'RAW' },
],
condition: { field: 'operation', value: ['write', 'batch_update'] },
condition: { field: 'operation', value: ['write', 'update', 'batch_update'] },
},
// Update-specific Fields
{
@@ -896,11 +902,15 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
type: 'string',
description: 'Destination spreadsheet ID for copy',
},
filterColumn: { type: 'string', description: 'Column header name to filter on' },
filterColumn: {
type: 'string',
description: 'Column header name to filter the read rows on (within the read range)',
},
filterValue: { type: 'string', description: 'Value to match against the filter column' },
filterMatchType: {
type: 'string',
description: 'Match type: contains, exact, starts_with, or ends_with',
description:
'Match type: contains, not_contains, exact, not_equals, starts_with, ends_with, gt, gte, lt, or lte',
},
},
outputs: {
@@ -920,6 +930,12 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
description: 'Cell values as 2D array',
condition: { field: 'operation', value: 'read' },
},
filter: {
type: 'json',
description:
'Filter summary (present only when a filter was requested): applied, column, matchType, columnFound, matchedRows, totalRows',
condition: { field: 'operation', value: 'read' },
},
// Write/Update/Append outputs
updatedRange: {
type: 'string',
+3 -3
View File
@@ -69,14 +69,14 @@ export const appendTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsAppendRe
const range = params.range || 'Sheet1'
const url = new URL(
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(range)}:append`
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(range)}:append`
)
// Default to USER_ENTERED if not specified
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
url.searchParams.append('valueInputOption', valueInputOption)
// Default to INSERT_ROWS if not specified
// Only send insertDataOption when the user provides it; the API defaults to OVERWRITE
if (params.insertDataOption) {
url.searchParams.append('insertDataOption', params.insertDataOption)
}
@@ -294,7 +294,7 @@ export const appendV2Tool: ToolConfig<GoogleSheetsV2ToolParams, GoogleSheetsV2Ap
}
const url = new URL(
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(sheetName)}:append`
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(sheetName)}:append`
)
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
+228
View File
@@ -0,0 +1,228 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { filterSheetRows } from '@/tools/google_sheets/filter'
const VALUES: unknown[][] = [
['Name', 'Email', 'Status', 'Score'],
['Alice', 'alice@example.com', 'Active', '90'],
['Bob', 'bob@test.com', 'Closed', '40'],
['Carol', 'carol@example.com', 'Active', '7'],
]
describe('filterSheetRows', () => {
it('passes values through unchanged when no filter column is provided', () => {
const result = filterSheetRows(VALUES, {})
expect(result.applied).toBe(false)
expect(result.values).toBe(VALUES)
expect(result.totalRows).toBe(3)
})
it('passes through when filterValue is empty', () => {
const result = filterSheetRows(VALUES, { filterColumn: 'Status', filterValue: '' })
expect(result.applied).toBe(false)
expect(result.values).toBe(VALUES)
})
it('defaults to case-insensitive contains', () => {
const result = filterSheetRows(VALUES, { filterColumn: 'status', filterValue: 'active' })
expect(result.applied).toBe(true)
expect(result.columnFound).toBe(true)
expect(result.values).toEqual([VALUES[0], VALUES[1], VALUES[3]])
expect(result.matchedRows).toBe(2)
})
it('matches column names case-insensitively and trims whitespace', () => {
const result = filterSheetRows(VALUES, {
filterColumn: ' EMAIL ',
filterValue: 'example.com',
})
expect(result.columnFound).toBe(true)
expect(result.matchedRows).toBe(2)
})
it('supports exact and not_equals', () => {
expect(
filterSheetRows(VALUES, {
filterColumn: 'Status',
filterValue: 'Active',
filterMatchType: 'exact',
}).matchedRows
).toBe(2)
expect(
filterSheetRows(VALUES, {
filterColumn: 'Status',
filterValue: 'Active',
filterMatchType: 'not_equals',
}).matchedRows
).toBe(1)
})
it('supports starts_with, ends_with, and not_contains', () => {
expect(
filterSheetRows(VALUES, {
filterColumn: 'Email',
filterValue: 'bob',
filterMatchType: 'starts_with',
}).matchedRows
).toBe(1)
expect(
filterSheetRows(VALUES, {
filterColumn: 'Email',
filterValue: '.com',
filterMatchType: 'ends_with',
}).matchedRows
).toBe(3)
expect(
filterSheetRows(VALUES, {
filterColumn: 'Email',
filterValue: 'example.com',
filterMatchType: 'not_contains',
}).matchedRows
).toBe(1)
})
it('compares numerically for ordering operators (not substring)', () => {
const gt = filterSheetRows(VALUES, {
filterColumn: 'Score',
filterValue: '50',
filterMatchType: 'gt',
})
expect(gt.matchedRows).toBe(1)
expect(gt.values).toEqual([VALUES[0], VALUES[1]])
expect(
filterSheetRows(VALUES, {
filterColumn: 'Score',
filterValue: '40',
filterMatchType: 'gte',
}).matchedRows
).toBe(2)
expect(
filterSheetRows(VALUES, {
filterColumn: 'Score',
filterValue: '40',
filterMatchType: 'lt',
}).matchedRows
).toBe(1)
expect(
filterSheetRows(VALUES, {
filterColumn: 'Score',
filterValue: '40',
filterMatchType: 'lte',
}).matchedRows
).toBe(2)
})
it('orders negative numbers correctly', () => {
const temps: unknown[][] = [
['City', 'Temp'],
['A', '-5'],
['B', '0'],
['C', '-12'],
['D', '3'],
]
expect(
filterSheetRows(temps, { filterColumn: 'Temp', filterValue: '-5', filterMatchType: 'gte' })
.matchedRows
).toBe(3)
expect(
filterSheetRows(temps, { filterColumn: 'Temp', filterValue: '0', filterMatchType: 'lt' })
.matchedRows
).toBe(2)
})
it('excludes blank and non-numeric cells from numeric comparisons', () => {
const scores: unknown[][] = [
['Name', 'Score'],
['Alice', '90'],
['Bob', ''],
['Carol', 'N/A'],
['Dan', '60'],
]
const result = filterSheetRows(scores, {
filterColumn: 'Score',
filterValue: '50',
filterMatchType: 'gt',
})
expect(result.matchedRows).toBe(2)
expect(result.values).toEqual([scores[0], scores[1], scores[4]])
})
it('falls back to lexicographic ordering when values are not numeric (ISO dates)', () => {
const dated: unknown[][] = [
['Task', 'Due'],
['A', '2026-01-15'],
['B', '2026-03-01'],
['C', '2025-12-31'],
]
const result = filterSheetRows(dated, {
filterColumn: 'Due',
filterValue: '2026-01-01',
filterMatchType: 'gte',
})
expect(result.matchedRows).toBe(2)
})
it('reports columnFound=false and leaves values unchanged when the column is missing', () => {
const result = filterSheetRows(VALUES, {
filterColumn: 'Nonexistent',
filterValue: 'x',
})
expect(result.applied).toBe(false)
expect(result.columnFound).toBe(false)
expect(result.matchedRows).toBe(0)
expect(result.values).toBe(VALUES)
expect(result.totalRows).toBe(3)
})
it('handles a header-only sheet and reports the column as found when it exists', () => {
const headerOnly: unknown[][] = [['Name', 'Status']]
const result = filterSheetRows(headerOnly, { filterColumn: 'Status', filterValue: 'Active' })
expect(result.applied).toBe(false)
expect(result.columnFound).toBe(true)
expect(result.matchedRows).toBe(0)
expect(result.totalRows).toBe(0)
expect(result.values).toBe(headerOnly)
})
it('reports columnFound=false for a header-only sheet when the column is absent', () => {
const headerOnly: unknown[][] = [['Name', 'Status']]
const result = filterSheetRows(headerOnly, { filterColumn: 'Nonexistent', filterValue: 'x' })
expect(result.applied).toBe(false)
expect(result.columnFound).toBe(false)
expect(result.matchedRows).toBe(0)
expect(result.values).toBe(headerOnly)
})
it('reports columnFound=false for an empty values array', () => {
const empty: unknown[][] = []
const result = filterSheetRows(empty, { filterColumn: 'Status', filterValue: 'Active' })
expect(result.applied).toBe(false)
expect(result.columnFound).toBe(false)
expect(result.matchedRows).toBe(0)
expect(result.totalRows).toBe(0)
})
it('treats missing cells as empty strings', () => {
const sparse: unknown[][] = [['Name', 'Status'], ['Alice'], ['Bob', 'Active']]
const result = filterSheetRows(sparse, {
filterColumn: 'Status',
filterValue: 'Active',
filterMatchType: 'exact',
})
expect(result.matchedRows).toBe(1)
expect(result.values).toEqual([sparse[0], sparse[2]])
})
it('always retains the header row in filtered output', () => {
const result = filterSheetRows(VALUES, {
filterColumn: 'Status',
filterValue: 'no-match',
filterMatchType: 'exact',
})
expect(result.values).toEqual([VALUES[0]])
expect(result.matchedRows).toBe(0)
})
})
+152
View File
@@ -0,0 +1,152 @@
/**
* Client-side row filtering for Google Sheets read results.
*
* The Google Sheets REST API (`spreadsheets.values.get`) has no server-side
* content filtering — `DataFilter` selects only by A1 range, grid range, or
* developer metadata, never by cell value. Filtering by cell content must
* therefore happen after values are fetched, over the window of rows the read
* returned (e.g. the default `A1:Z1000`), not the entire sheet.
*/
/**
* Supported ways to compare a cell against the filter value. Text operators are
* case-insensitive. The ordering operators (`gt`/`gte`/`lt`/`lte`) compare
* numerically when both operands parse as finite numbers, fall back to
* case-insensitive lexicographic comparison when both are non-numeric (which
* orders ISO dates correctly), and never match when one side is numeric and the
* other is not (the values are not comparable).
*/
export type SheetFilterMatchType =
| 'contains'
| 'not_contains'
| 'exact'
| 'not_equals'
| 'starts_with'
| 'ends_with'
| 'gt'
| 'gte'
| 'lt'
| 'lte'
export interface SheetFilterOptions {
filterColumn?: string
filterValue?: string
filterMatchType?: SheetFilterMatchType
}
export interface SheetFilterResult {
/** The (possibly filtered) values, always including the header row when present. */
values: unknown[][]
/** Whether row filtering was actually applied to the data rows. */
applied: boolean
/** Whether the requested filter column was found in the header row. */
columnFound: boolean
/** Number of data rows (excluding the header) that matched the filter. */
matchedRows: number
/** Total number of data rows (excluding the header) that were considered. */
totalRows: number
}
const DEFAULT_MATCH_TYPE: SheetFilterMatchType = 'contains'
/**
* Parses a cell string as a finite number, or returns null when it is blank or
* non-numeric so callers can fall back to lexicographic comparison.
*/
function asFiniteNumber(value: string): number | null {
if (value.trim() === '') return null
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
/** Case-insensitive lexicographic comparison returning -1, 0, or 1. */
function compareLexicographic(cell: string, target: string): number {
return Math.sign(cell.toLowerCase().localeCompare(target.toLowerCase()))
}
/** Evaluates a single cell against the filter target for the given match type. */
function matchesCell(cell: string, target: string, matchType: SheetFilterMatchType): boolean {
switch (matchType) {
case 'gt':
case 'gte':
case 'lt':
case 'lte': {
const cellNum = asFiniteNumber(cell)
const targetNum = asFiniteNumber(target)
let cmp: number
if (cellNum !== null && targetNum !== null) {
cmp = Math.sign(cellNum - targetNum)
} else if (cellNum === null && targetNum === null) {
cmp = compareLexicographic(cell, target)
} else {
return false
}
if (matchType === 'gt') return cmp > 0
if (matchType === 'gte') return cmp >= 0
if (matchType === 'lt') return cmp < 0
return cmp <= 0
}
case 'exact':
return cell.toLowerCase() === target.toLowerCase()
case 'not_equals':
return cell.toLowerCase() !== target.toLowerCase()
case 'starts_with':
return cell.toLowerCase().startsWith(target.toLowerCase())
case 'ends_with':
return cell.toLowerCase().endsWith(target.toLowerCase())
case 'not_contains':
return !cell.toLowerCase().includes(target.toLowerCase())
default:
return cell.toLowerCase().includes(target.toLowerCase())
}
}
/**
* Filters a 2D values array (header row + data rows) by matching a single column
* against a target value. Returns the original values untouched when no filter
* is requested, when there are no data rows, or when the column is not found —
* the `applied`/`columnFound` flags let callers distinguish "no match possible"
* from "everything matched".
*/
export function filterSheetRows(
values: unknown[][],
options: SheetFilterOptions
): SheetFilterResult {
const { filterColumn, filterValue, filterMatchType } = options
const totalRows = Math.max(values.length - 1, 0)
if (!filterColumn || filterValue === undefined || filterValue === '') {
return { values, applied: false, columnFound: true, matchedRows: totalRows, totalRows }
}
const headers = values[0] ?? []
const normalizedColumn = filterColumn.trim().toLowerCase()
const columnIndex = headers.findIndex(
(header) => String(header).trim().toLowerCase() === normalizedColumn
)
const columnFound = columnIndex !== -1
// No data rows to evaluate (empty or header-only sheet): nothing matched, but
// still report whether the requested column actually exists in the header.
if (values.length <= 1) {
return { values, applied: false, columnFound, matchedRows: 0, totalRows: 0 }
}
// Column not found: leave rows untouched and report zero matches, not totalRows.
if (!columnFound) {
return { values, applied: false, columnFound: false, matchedRows: 0, totalRows }
}
const matchType = filterMatchType ?? DEFAULT_MATCH_TYPE
const matched = values
.slice(1)
.filter((row) => matchesCell(String(row[columnIndex] ?? ''), filterValue, matchType))
return {
values: [values[0], ...matched],
applied: true,
columnFound: true,
matchedRows: matched.length,
totalRows,
}
}
+97
View File
@@ -0,0 +1,97 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { readV2Tool } from '@/tools/google_sheets/read'
import type { GoogleSheetsV2ToolParams } from '@/tools/google_sheets/types'
const SPREADSHEET_ID = 'abc123'
const URL = `https://sheets.googleapis.com/v4/spreadsheets/${SPREADSHEET_ID}/values/Sheet1!A1:Z1000`
const SHEET_DATA = {
range: 'Sheet1!A1:D4',
values: [
['Name', 'Status'],
['Alice', 'Active'],
['Bob', 'Closed'],
],
}
function mockResponse(body: unknown, url = URL): Response {
// double-cast-allowed: lightweight Response stub for transformResponse unit test
return { url, json: async () => body } as unknown as Response
}
const baseParams: GoogleSheetsV2ToolParams = {
accessToken: 'token',
spreadsheetId: SPREADSHEET_ID,
sheetName: 'Sheet1',
}
describe('readV2Tool.transformResponse', () => {
it('returns values untouched and omits the filter field when no filter is requested', async () => {
const result = await readV2Tool.transformResponse!(mockResponse(SHEET_DATA), baseParams)
expect(result.output.values).toEqual(SHEET_DATA.values)
expect('filter' in result.output).toBe(false)
expect(result.output.range).toBe(SHEET_DATA.range)
expect(result.output.metadata.spreadsheetId).toBe(SPREADSHEET_ID)
})
it('omits the filter field when filterColumn is set but filterValue is empty', async () => {
const result = await readV2Tool.transformResponse!(mockResponse(SHEET_DATA), {
...baseParams,
filterColumn: 'Status',
filterValue: '',
})
expect('filter' in result.output).toBe(false)
expect(result.output.values).toEqual(SHEET_DATA.values)
})
it('filters rows and reports filter metadata when a filter is applied', async () => {
const result = await readV2Tool.transformResponse!(mockResponse(SHEET_DATA), {
...baseParams,
filterColumn: 'Status',
filterValue: 'Active',
filterMatchType: 'exact',
})
expect(result.output.values).toEqual([
['Name', 'Status'],
['Alice', 'Active'],
])
expect(result.output.filter).toEqual({
applied: true,
column: 'Status',
matchType: 'exact',
columnFound: true,
matchedRows: 1,
totalRows: 2,
})
})
it('leaves values unchanged and reports columnFound=false when the column is missing', async () => {
const result = await readV2Tool.transformResponse!(mockResponse(SHEET_DATA), {
...baseParams,
filterColumn: 'Nonexistent',
filterValue: 'x',
})
expect(result.output.values).toEqual(SHEET_DATA.values)
expect(result.output.filter?.columnFound).toBe(false)
expect(result.output.filter?.applied).toBe(false)
expect(result.output.filter?.matchedRows).toBe(0)
})
it('handles a response with no values array', async () => {
const result = await readV2Tool.transformResponse!(mockResponse({ range: 'Sheet1!A1' }), {
...baseParams,
filterColumn: 'Status',
filterValue: 'Active',
})
expect(result.output.values).toEqual([])
expect(result.output.filter?.applied).toBe(false)
})
})
+27 -33
View File
@@ -1,3 +1,4 @@
import { filterSheetRows } from '@/tools/google_sheets/filter'
import type {
GoogleSheetsReadResponse,
GoogleSheetsToolParams,
@@ -53,7 +54,7 @@ export const readTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsReadRespon
// Keep a generous column/row bound to avoid huge payloads
if (!params.range) {
const defaultRange = 'A1:Z1000'
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${defaultRange}`
return `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(defaultRange)}`
}
// Otherwise, get values from the specified range
@@ -159,7 +160,7 @@ export const readV2Tool: ToolConfig<GoogleSheetsV2ToolParams, GoogleSheetsV2Read
required: false,
visibility: 'user-or-llm',
description:
'Column name (from header row) to filter on. If not provided, no filtering is applied.',
'Column name (from the header row) to filter on. Filtering is applied to the rows returned by the read range (the default is A1:Z1000), not the entire sheet. If not provided, no filtering is applied.',
},
filterValue: {
type: 'string',
@@ -172,7 +173,7 @@ export const readV2Tool: ToolConfig<GoogleSheetsV2ToolParams, GoogleSheetsV2Read
required: false,
visibility: 'user-or-llm',
description:
'How to match the filter value: "contains", "exact", "starts_with", or "ends_with". Defaults to "contains".',
'How to match the filter value. Text: "contains", "not_contains", "exact", "not_equals", "starts_with", "ends_with". Numeric/ordering: "gt", "gte", "lt", "lte" (numeric when both values are numbers, otherwise lexicographic). Defaults to "contains".',
},
},
@@ -216,48 +217,41 @@ export const readV2Tool: ToolConfig<GoogleSheetsV2ToolParams, GoogleSheetsV2Read
spreadsheetUrl: `https://docs.google.com/spreadsheets/d/${spreadsheetId}`,
}
let values: unknown[][] = data.values ?? []
const rawValues: unknown[][] = data.values ?? []
// Apply client-side filtering only when both filterColumn and filterValue are provided
if (params?.filterColumn && params?.filterValue !== undefined && values.length > 1) {
const headers = values[0] as string[]
const columnIndex = headers.findIndex(
(h) => String(h).toLowerCase() === params.filterColumn!.toLowerCase()
)
const filterRequested =
Boolean(params?.filterColumn) &&
params?.filterValue !== undefined &&
params?.filterValue !== ''
if (columnIndex !== -1) {
const matchType = params.filterMatchType ?? 'contains'
const filterVal = params.filterValue.toLowerCase()
const filteredRows = values.slice(1).filter((row) => {
const cellValue = String(row[columnIndex] ?? '').toLowerCase()
switch (matchType) {
case 'exact':
return cellValue === filterVal
case 'starts_with':
return cellValue.startsWith(filterVal)
case 'ends_with':
return cellValue.endsWith(filterVal)
default:
return cellValue.includes(filterVal)
}
})
// Return header row + matching rows
values = [values[0], ...filteredRows]
}
}
const filterResult = filterSheetRows(rawValues, {
filterColumn: params?.filterColumn,
filterValue: params?.filterValue,
filterMatchType: params?.filterMatchType,
})
return {
success: true,
output: {
sheetName: params?.sheetName ?? '',
range: data.range ?? '',
values,
values: filterResult.values,
metadata: {
spreadsheetId: metadata.spreadsheetId,
spreadsheetUrl: metadata.spreadsheetUrl,
},
...(filterRequested
? {
filter: {
applied: filterResult.applied,
column: params?.filterColumn ?? '',
matchType: params?.filterMatchType ?? 'contains',
columnFound: filterResult.columnFound,
matchedRows: filterResult.matchedRows,
totalRows: filterResult.totalRows,
},
}
: {}),
},
}
},
+18 -1
View File
@@ -1,6 +1,7 @@
import type { GoogleSheetsV2DeleteRowsResponse } from '@/tools/google_sheets/delete_rows'
import type { GoogleSheetsV2DeleteSheetResponse } from '@/tools/google_sheets/delete_sheet'
import type { GoogleSheetsV2DeleteSpreadsheetResponse } from '@/tools/google_sheets/delete_spreadsheet'
import type { SheetFilterMatchType } from '@/tools/google_sheets/filter'
import type { ToolResponse } from '@/tools/types'
interface GoogleSheetsRange {
@@ -81,12 +82,28 @@ export type GoogleSheetsResponse =
// V2 Types - with explicit sheetName parameter
export interface GoogleSheetsFilterInfo {
/** Whether row filtering was actually applied to the data rows. */
applied: boolean
/** The column header the filter targeted. */
column: string
/** The match type used for the comparison. */
matchType: SheetFilterMatchType
/** Whether the requested filter column was found in the header row. */
columnFound: boolean
/** Number of data rows (excluding the header) that matched the filter. */
matchedRows: number
/** Total number of data rows (excluding the header) that were considered. */
totalRows: number
}
export interface GoogleSheetsV2ReadResponse extends ToolResponse {
output: {
sheetName: string
range: string
values: any[][]
metadata: GoogleSheetsMetadata
filter?: GoogleSheetsFilterInfo
}
}
@@ -134,7 +151,7 @@ export interface GoogleSheetsV2ToolParams {
majorDimension?: 'ROWS' | 'COLUMNS'
filterColumn?: string
filterValue?: string
filterMatchType?: 'contains' | 'exact' | 'starts_with' | 'ends_with'
filterMatchType?: SheetFilterMatchType
}
export type GoogleSheetsV2Response =
+2 -2
View File
@@ -63,7 +63,7 @@ export const updateTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsUpdateRe
const range = params.range || 'Sheet1!A2'
const url = new URL(
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(range)}`
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(range)}`
)
// Default to USER_ENTERED if not specified
@@ -251,7 +251,7 @@ export const updateV2Tool: ToolConfig<GoogleSheetsV2ToolParams, GoogleSheetsV2Up
const fullRange = `${sheetName}!${cellRange}`
const url = new URL(
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(fullRange)}`
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(fullRange)}`
)
const valueInputOption = params.valueInputOption || 'USER_ENTERED'
+2 -2
View File
@@ -63,7 +63,7 @@ export const writeTool: ToolConfig<GoogleSheetsToolParams, GoogleSheetsWriteResp
const range = params.range || 'Sheet1!A2'
const url = new URL(
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(range)}`
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(range)}`
)
// Default to USER_ENTERED if not specified
@@ -253,7 +253,7 @@ export const writeV2Tool: ToolConfig<GoogleSheetsV2ToolParams, GoogleSheetsV2Wri
const fullRange = `${sheetName}!${cellRange}`
const url = new URL(
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId}/values/${encodeURIComponent(fullRange)}`
`https://sheets.googleapis.com/v4/spreadsheets/${params.spreadsheetId?.trim()}/values/${encodeURIComponent(fullRange)}`
)
const valueInputOption = params.valueInputOption || 'USER_ENTERED'