* Fetches the sheet (A:Z) and returns the row count, auto-detected headers, * and the 1-indexed header row number in a single API call. * * The Sheets API omits trailing empty rows, so `rows.length` equals the last * non-empty row in columns A–Z. Header detection scans the first * {@link HEADER
( accessToken: string, spreadsheetId: string, sheetName: string, valueRenderOption: ValueRenderOption, dateTimeRenderOption: DateTimeRenderOption, requestId: string, logger: Logger )
| 296 | * `headerRowIndex = 0` when no header is found within the scan window. |
| 297 | */ |
| 298 | async function fetchSheetState( |
| 299 | accessToken: string, |
| 300 | spreadsheetId: string, |
| 301 | sheetName: string, |
| 302 | valueRenderOption: ValueRenderOption, |
| 303 | dateTimeRenderOption: DateTimeRenderOption, |
| 304 | requestId: string, |
| 305 | logger: Logger |
| 306 | ): Promise<{ rowCount: number; headers: string[]; headerRowIndex: number }> { |
| 307 | const encodedSheet = encodeURIComponent(sheetName) |
| 308 | const params = new URLSearchParams({ |
| 309 | majorDimension: 'ROWS', |
| 310 | fields: 'values', |
| 311 | valueRenderOption, |
| 312 | dateTimeRenderOption, |
| 313 | }) |
| 314 | const url = `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodedSheet}!A:Z?${params.toString()}` |
| 315 | |
| 316 | const response = await fetch(url, { |
| 317 | headers: { Authorization: `Bearer ${accessToken}` }, |
| 318 | }) |
| 319 | |
| 320 | if (!response.ok) { |
| 321 | const status = response.status |
| 322 | const errorData = await response.json().catch(() => ({})) |
| 323 | if (status === 403 || status === 429) { |
| 324 | throw new Error( |
| 325 | `Sheets API rate limit (${status}) — skipping to retry next poll cycle: ${JSON.stringify(errorData)}` |
| 326 | ) |
| 327 | } |
| 328 | throw new Error( |
| 329 | `Failed to fetch sheet state: ${status} ${response.statusText} - ${JSON.stringify(errorData)}` |
| 330 | ) |
| 331 | } |
| 332 | |
| 333 | const data = await response.json() |
| 334 | const rows = (data.values as string[][] | undefined) ?? [] |
| 335 | const rowCount = rows.length |
| 336 | |
| 337 | let headers: string[] = [] |
| 338 | let headerRowIndex = 0 |
| 339 | for (let i = 0; i < Math.min(rows.length, HEADER_SCAN_ROWS); i++) { |
| 340 | const row = rows[i] |
| 341 | if (row?.some((cell) => cell !== '')) { |
| 342 | headers = row |
| 343 | headerRowIndex = i + 1 |
| 344 | break |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | return { rowCount, headers, headerRowIndex } |
| 349 | } |
| 350 | |
| 351 | async function fetchRowRange( |
| 352 | accessToken: string, |