From de44178d849f3e6b5824bfc2da4918bf89696bd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 05:26:21 +0000 Subject: [PATCH 1/5] feat(google-sheets): add hidden row state to SheetTable.getData() result Add Sheet.getRowMetadata() to fetch hiddenByUser/hiddenByFilter from Google Sheets API rowMetadata. Each row returned by SheetTable.getData() now includes _hiddenByUser and _hiddenByFilter boolean flags. Closes #865 https://claude.ai/code/session_01RTFQ6jWKWVC5vFUBQPoAm1 --- .../@d-zero/google-sheets/src/sheet-table.ts | 25 ++++++++++++------- .../@d-zero/google-sheets/src/sheets/sheet.ts | 12 +++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/@d-zero/google-sheets/src/sheet-table.ts b/packages/@d-zero/google-sheets/src/sheet-table.ts index c2b91a23..f6fd5266 100644 --- a/packages/@d-zero/google-sheets/src/sheet-table.ts +++ b/packages/@d-zero/google-sheets/src/sheet-table.ts @@ -112,28 +112,35 @@ export class SheetTable { typeMap.set(firstColIndex + cellType.index, cellType.type); } - // Get all data - const data = await this.#sheet.getValues( - `${headers.at(0)?.row ?? 'A'}${this.#bodyStartRow}`, - headers.at(-1)?.row ?? 'A', - ); + // Get all data and row metadata in parallel + const [data, rowMetadata] = await Promise.all([ + this.#sheet.getValues( + `${headers.at(0)?.row ?? 'A'}${this.#bodyStartRow}`, + headers.at(-1)?.row ?? 'A', + ), + this.#sheet.getRowMetadata(this.#bodyStartRow), + ]); if (data == null) { return []; } - const list = data.map((_d) => { - const _data: Partial = {}; + const list = data.map((_d, rowIndex) => { + const meta = rowMetadata[rowIndex]; + const _data: Record = { + _hiddenByUser: meta?.hiddenByUser ?? false, + _hiddenByFilter: meta?.hiddenByFilter ?? false, + }; for (const header of headers) { const relativeIndex = header.index - firstColIndex; const rawValue = _d[relativeIndex]; const cellType = typeMap.get(header.index) ?? 'string'; - _data[header.key] = convertValue(rawValue, cellType) as T[keyof T]; + _data[header.key as string] = convertValue(rawValue, cellType); } - return _data as T; + return _data as T & { _hiddenByUser: boolean; _hiddenByFilter: boolean }; }); return list; diff --git a/packages/@d-zero/google-sheets/src/sheets/sheet.ts b/packages/@d-zero/google-sheets/src/sheets/sheet.ts index 29853fcb..00966bfc 100644 --- a/packages/@d-zero/google-sheets/src/sheets/sheet.ts +++ b/packages/@d-zero/google-sheets/src/sheets/sheet.ts @@ -168,6 +168,18 @@ export class Sheet { return index; } + async getRowMetadata(startRow: number) { + const res = await this.#parent.getWithGridData( + `'${this.props.title}'!A${startRow}:A`, + ); + const sheet = res.data.sheets?.[0]; + const rowMetadataList = sheet?.data?.[0]?.rowMetadata ?? []; + return rowMetadataList.map((metadata) => ({ + hiddenByUser: metadata.hiddenByUser === true, + hiddenByFilter: metadata.hiddenByFilter === true, + })); + } + async getValues(row: string, col: string) { const res = await this.#parent.get({ range: `'${this.props.title}'!${row}:${col}`, From 5d1680bf46279f802c689b7ce703a797e2d01719 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 05:28:03 +0000 Subject: [PATCH 2/5] refactor(google-sheets): remove underscore prefix from hidden row flags Rename _hiddenByUser/_hiddenByFilter to hiddenByUser/hiddenByFilter to match Google Sheets API property names directly. https://claude.ai/code/session_01RTFQ6jWKWVC5vFUBQPoAm1 --- packages/@d-zero/google-sheets/src/sheet-table.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@d-zero/google-sheets/src/sheet-table.ts b/packages/@d-zero/google-sheets/src/sheet-table.ts index f6fd5266..5b3bf650 100644 --- a/packages/@d-zero/google-sheets/src/sheet-table.ts +++ b/packages/@d-zero/google-sheets/src/sheet-table.ts @@ -128,8 +128,8 @@ export class SheetTable { const list = data.map((_d, rowIndex) => { const meta = rowMetadata[rowIndex]; const _data: Record = { - _hiddenByUser: meta?.hiddenByUser ?? false, - _hiddenByFilter: meta?.hiddenByFilter ?? false, + hiddenByUser: meta?.hiddenByUser ?? false, + hiddenByFilter: meta?.hiddenByFilter ?? false, }; for (const header of headers) { @@ -140,7 +140,7 @@ export class SheetTable { _data[header.key as string] = convertValue(rawValue, cellType); } - return _data as T & { _hiddenByUser: boolean; _hiddenByFilter: boolean }; + return _data as T & { hiddenByUser: boolean; hiddenByFilter: boolean }; }); return list; From ace2a4134b221ceec3c417964900718e1136a6bf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 05:34:49 +0000 Subject: [PATCH 3/5] test(google-sheets): add tests for Sheet.getRowMetadata() Add unit tests covering hiddenByUser/hiddenByFilter combinations, empty rowMetadata, missing sheets data, and range parameter passing. Also improve type safety of getData() by using spread instead of Record. https://claude.ai/code/session_01RTFQ6jWKWVC5vFUBQPoAm1 --- .../@d-zero/google-sheets/src/sheet-table.ts | 13 ++- .../google-sheets/src/sheets/sheet.spec.ts | 107 ++++++++++++++++++ 2 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 packages/@d-zero/google-sheets/src/sheets/sheet.spec.ts diff --git a/packages/@d-zero/google-sheets/src/sheet-table.ts b/packages/@d-zero/google-sheets/src/sheet-table.ts index 5b3bf650..2946f1e0 100644 --- a/packages/@d-zero/google-sheets/src/sheet-table.ts +++ b/packages/@d-zero/google-sheets/src/sheet-table.ts @@ -127,20 +127,21 @@ export class SheetTable { const list = data.map((_d, rowIndex) => { const meta = rowMetadata[rowIndex]; - const _data: Record = { - hiddenByUser: meta?.hiddenByUser ?? false, - hiddenByFilter: meta?.hiddenByFilter ?? false, - }; + const _data: Partial = {}; for (const header of headers) { const relativeIndex = header.index - firstColIndex; const rawValue = _d[relativeIndex]; const cellType = typeMap.get(header.index) ?? 'string'; - _data[header.key as string] = convertValue(rawValue, cellType); + _data[header.key] = convertValue(rawValue, cellType) as T[keyof T]; } - return _data as T & { hiddenByUser: boolean; hiddenByFilter: boolean }; + return { + ..._data, + hiddenByUser: meta?.hiddenByUser ?? false, + hiddenByFilter: meta?.hiddenByFilter ?? false, + } as T & { hiddenByUser: boolean; hiddenByFilter: boolean }; }); return list; diff --git a/packages/@d-zero/google-sheets/src/sheets/sheet.spec.ts b/packages/@d-zero/google-sheets/src/sheets/sheet.spec.ts new file mode 100644 index 00000000..6069b664 --- /dev/null +++ b/packages/@d-zero/google-sheets/src/sheets/sheet.spec.ts @@ -0,0 +1,107 @@ +import { describe, test, expect, vi } from 'vitest'; + +import { Sheet } from './sheet.js'; + +/** + * + * @param rowMetadata + */ +function createMockParent(rowMetadata: Record[]) { + return { + getWithGridData: vi.fn().mockResolvedValue({ + data: { + sheets: [ + { + data: [ + { + rowMetadata, + }, + ], + }, + ], + }, + }), + }; +} + +describe('getRowMetadata', () => { + const mockSheet = { + properties: { title: 'TestSheet', sheetId: 0 }, + }; + + test('returns hiddenByUser: true for user-hidden rows', async () => { + const parent = createMockParent([{ hiddenByUser: true }, { hiddenByUser: false }]); + const sheet = new Sheet(mockSheet as never, parent as never); + + const result = await sheet.getRowMetadata(2); + + expect(result).toEqual([ + { hiddenByUser: true, hiddenByFilter: false }, + { hiddenByUser: false, hiddenByFilter: false }, + ]); + }); + + test('returns hiddenByFilter: true for filter-hidden rows', async () => { + const parent = createMockParent([ + { hiddenByFilter: true }, + { hiddenByFilter: false }, + ]); + const sheet = new Sheet(mockSheet as never, parent as never); + + const result = await sheet.getRowMetadata(2); + + expect(result).toEqual([ + { hiddenByUser: false, hiddenByFilter: true }, + { hiddenByUser: false, hiddenByFilter: false }, + ]); + }); + + test('returns both true when hidden by user and filter', async () => { + const parent = createMockParent([{ hiddenByUser: true, hiddenByFilter: true }]); + const sheet = new Sheet(mockSheet as never, parent as never); + + const result = await sheet.getRowMetadata(2); + + expect(result).toEqual([{ hiddenByUser: true, hiddenByFilter: true }]); + }); + + test('returns both false for visible rows', async () => { + const parent = createMockParent([{}]); + const sheet = new Sheet(mockSheet as never, parent as never); + + const result = await sheet.getRowMetadata(2); + + expect(result).toEqual([{ hiddenByUser: false, hiddenByFilter: false }]); + }); + + test('returns empty array when rowMetadata is empty', async () => { + const parent = createMockParent([]); + const sheet = new Sheet(mockSheet as never, parent as never); + + const result = await sheet.getRowMetadata(2); + + expect(result).toEqual([]); + }); + + test('returns empty array when API returns no sheets data', async () => { + const parent = { + getWithGridData: vi.fn().mockResolvedValue({ + data: { sheets: [] }, + }), + }; + const sheet = new Sheet(mockSheet as never, parent as never); + + const result = await sheet.getRowMetadata(2); + + expect(result).toEqual([]); + }); + + test('passes correct range to getWithGridData', async () => { + const parent = createMockParent([]); + const sheet = new Sheet(mockSheet as never, parent as never); + + await sheet.getRowMetadata(3); + + expect(parent.getWithGridData).toHaveBeenCalledWith("'TestSheet'!A3:A"); + }); +}); From 3661884d739aa9116cc55f046ef6cf71ba541f79 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 05:51:39 +0000 Subject: [PATCH 4/5] docs(google-sheets): add JSDoc for getRowMetadata() and getData() Document hiddenByUser/hiddenByFilter semantics and add reference link to Google Sheets API DimensionProperties. https://claude.ai/code/session_01RTFQ6jWKWVC5vFUBQPoAm1 --- packages/@d-zero/google-sheets/src/sheet-table.ts | 8 ++++++++ packages/@d-zero/google-sheets/src/sheets/sheet.ts | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/@d-zero/google-sheets/src/sheet-table.ts b/packages/@d-zero/google-sheets/src/sheet-table.ts index 2946f1e0..23f701ca 100644 --- a/packages/@d-zero/google-sheets/src/sheet-table.ts +++ b/packages/@d-zero/google-sheets/src/sheet-table.ts @@ -84,6 +84,14 @@ export class SheetTable { ); } + /** + * Retrieves all data rows with header-mapped values and row visibility state. + * + * Each returned record includes: + * - `hiddenByUser` (`boolean`): `true` if the row is manually hidden by a user + * - `hiddenByFilter` (`boolean`): `true` if the row is hidden by a filter view + * @returns Array of records typed as `T & { hiddenByUser: boolean; hiddenByFilter: boolean }` + */ async getData() { if (!this.#sheet) { throw new Error('Sheet is not created'); diff --git a/packages/@d-zero/google-sheets/src/sheets/sheet.ts b/packages/@d-zero/google-sheets/src/sheets/sheet.ts index 00966bfc..f5b12bd4 100644 --- a/packages/@d-zero/google-sheets/src/sheets/sheet.ts +++ b/packages/@d-zero/google-sheets/src/sheets/sheet.ts @@ -168,6 +168,14 @@ export class Sheet { return index; } + /** + * Retrieves row visibility metadata starting from the specified row. + * + * - `hiddenByUser`: The row is manually hidden by a user (right-click → "Hide row") + * - `hiddenByFilter`: The row is hidden by a filter view or filter condition + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#DimensionProperties + * @param startRow - The 1-based row number to start fetching metadata from + */ async getRowMetadata(startRow: number) { const res = await this.#parent.getWithGridData( `'${this.props.title}'!A${startRow}:A`, From 86aa6613fb98bd11966a2d37fb2ed44f113ef83e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 05:57:55 +0000 Subject: [PATCH 5/5] docs(google-sheets): add JSDoc to exported types, classes, and functions Add documentation for HeaderCell, SheetTableOptions, SearchTableHeaders, DefineHeader, SheetTable class and its public methods, and internal helper functions (getHeaders, getClmName). Add @returns to getRowMetadata. https://claude.ai/code/session_01RTFQ6jWKWVC5vFUBQPoAm1 --- .../@d-zero/google-sheets/src/sheet-table.ts | 49 ++++++++++++++++--- .../@d-zero/google-sheets/src/sheets/sheet.ts | 1 + 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/@d-zero/google-sheets/src/sheet-table.ts b/packages/@d-zero/google-sheets/src/sheet-table.ts index 23f701ca..41a808cf 100644 --- a/packages/@d-zero/google-sheets/src/sheet-table.ts +++ b/packages/@d-zero/google-sheets/src/sheet-table.ts @@ -9,11 +9,17 @@ import { Sheets } from './sheets/sheets.js'; // Google Sheets epoch (December 30, 1899) const SHEETS_EPOCH = new Date(1899, 11, 30).getTime(); +/** + * Configuration for a single header column in a sheet table. + */ export type HeaderCell = { readonly label: string; readonly conditionalFormatRules?: sheets_v4.Schema$ConditionalFormatRule[]; }; +/** + * Options for creating a {@link SheetTable}. + */ export type SheetTableOptions = { readonly bodyStartRow?: number; readonly frozen?: { @@ -22,16 +28,30 @@ export type SheetTableOptions = { }; }; +/** + * Header configuration that searches for existing header labels in the sheet. + * @template T - Record type whose keys correspond to header labels in the sheet + */ export type SearchTableHeaders = { readonly headerRowNumber?: number; readonly search: readonly (keyof T)[]; }; +/** + * Header configuration that explicitly defines column headers and their labels. + * @template T - Record type whose keys are used as header identifiers + */ export type DefineHeader = { readonly headerRowNumber?: number; readonly define: Record; }; +/** + * Typed table interface for reading from and writing to a Google Sheets worksheet. + * + * Use the static {@link SheetTable.create} factory method to instantiate. + * @template T - Record type representing a single row of data + */ export class SheetTable { readonly #auth: OAuth2Client; readonly #bodyStartRow: number; @@ -67,6 +87,10 @@ export class SheetTable { this.#bodyStartRow = options?.bodyStartRow ?? 2; } + /** + * Appends rows to the sheet. + * @param records - Array of row data keyed by header identifiers + */ async addRecords(records: ReadonlyArray>) { if (!this.#sheet) { throw new Error('Sheet is not created'); @@ -193,6 +217,16 @@ export class SheetTable { } } + /** + * Creates and initializes a new {@link SheetTable} instance. + * @template T - Record type representing a single row of data + * @param sheetUrl - Full URL of the Google Spreadsheet + * @param sheetName - Name of the worksheet tab to operate on + * @param auth - Authenticated OAuth2 client for Google Sheets API access + * @param header - Header configuration (define columns explicitly or search existing ones) + * @param options - Additional table options such as frozen panes and body start row + * @returns Initialized SheetTable instance ready for read/write operations + */ static async create( sheetUrl: string, sheetName: string, @@ -213,10 +247,11 @@ interface Header { } /** - * - * @param sheet - * @param headerRowNumber - * @param keys + * Resolves header keys to their column positions by reading the header row from the sheet. + * @param sheet - Sheet instance to read headers from + * @param headerRowNumber - 1-based row number where headers are located + * @param keys - Header keys to search for in the header row + * @returns Resolved headers sorted by column index, excluding keys not found */ async function getHeaders( sheet: Sheet, @@ -241,9 +276,9 @@ async function getHeaders( } /** - * Convert column number to alphabet column name. - * Example: 5 => "E", 100 => "CV" - * @param col + * Converts a 1-based column number to an alphabetic column name (e.g. 5 → "E", 100 → "CV"). + * @param col - 1-based column number + * @returns Alphabetic column name */ function getClmName(col: number): string { const COUNT_OF_ALPHABET = 26; diff --git a/packages/@d-zero/google-sheets/src/sheets/sheet.ts b/packages/@d-zero/google-sheets/src/sheets/sheet.ts index f5b12bd4..a5c56ee1 100644 --- a/packages/@d-zero/google-sheets/src/sheets/sheet.ts +++ b/packages/@d-zero/google-sheets/src/sheets/sheet.ts @@ -175,6 +175,7 @@ export class Sheet { * - `hiddenByFilter`: The row is hidden by a filter view or filter condition * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/sheets#DimensionProperties * @param startRow - The 1-based row number to start fetching metadata from + * @returns Array of row visibility objects, one per row from `startRow` onward */ async getRowMetadata(startRow: number) { const res = await this.#parent.getWithGridData(