From 7e2feeb7b8caa3df1536c043063534a84c38a0f8 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Fri, 13 Feb 2026 23:08:42 -0800 Subject: [PATCH 1/2] feat: add searchDeveloperMetadata and support metadata filters in loadCells Co-Authored-By: Claude Opus 4.6 --- .changeset/developer-metadata-search.md | 5 ++++ docs/classes/google-spreadsheet-worksheet.md | 5 +++- docs/classes/google-spreadsheet.md | 17 +++++++++++ src/lib/GoogleSpreadsheet.ts | 31 ++++++++++++++++---- src/lib/GoogleSpreadsheetWorksheet.ts | 5 +++- src/lib/types/sheets-types.ts | 4 +-- src/test/manage.test.ts | 23 +++++++++++++++ 7 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 .changeset/developer-metadata-search.md diff --git a/.changeset/developer-metadata-search.md b/.changeset/developer-metadata-search.md new file mode 100644 index 0000000..6bf9ea5 --- /dev/null +++ b/.changeset/developer-metadata-search.md @@ -0,0 +1,5 @@ +--- +"google-spreadsheet": minor +--- + +Add searchDeveloperMetadata method and support DeveloperMetadataLookup filters in loadCells diff --git a/docs/classes/google-spreadsheet-worksheet.md b/docs/classes/google-spreadsheet-worksheet.md index 4d06cb1..d456924 100644 --- a/docs/classes/google-spreadsheet-worksheet.md +++ b/docs/classes/google-spreadsheet-worksheet.md @@ -153,7 +153,7 @@ The cell-based interface lets you load and update individual cells in a sheeet, !> This method does not return the cells it loads, instead they are kept in a local cache managed by the sheet. See methods below (`getCell` and `getCellByA1`) to access them. -You can filter the cells you want to fetch in several ways. See [Data Filters](https://developers.google.com/sheets/api/reference/rest/v4/DataFilter) for more info. Strings are treated as A1 ranges, objects are detected to be a [GridRange](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#GridRange) with sheetId not required. +You can filter the cells you want to fetch in several ways. See [Data Filters](https://developers.google.com/sheets/api/reference/rest/v4/DataFilter) for more info. Strings are treated as A1 ranges, objects are detected to be a [GridRange](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#GridRange) with sheetId not required. Objects with a `developerMetadataLookup` key are treated as [DeveloperMetadataLookup](https://developers.google.com/sheets/api/reference/rest/v4/DataFilter#DeveloperMetadataLookup) filters. ```javascript await sheet.loadCells(); // no filter - will load ALL cells in the sheet @@ -163,6 +163,9 @@ await sheet.loadCells({ // GridRange object }); await sheet.loadCells({ startRowIndex: 50 }); // not all props required await sheet.loadCells(['B2:D5', 'B50:D55']); // can pass an array of filters +await sheet.loadCells({ // DeveloperMetadataLookup filter + developerMetadataLookup: { metadataKey: 'my-key' } +}); ``` !> If using an API key (read-only access), only A1 ranges are supported diff --git a/docs/classes/google-spreadsheet.md b/docs/classes/google-spreadsheet.md index b3f4e65..84416ea 100644 --- a/docs/classes/google-spreadsheet.md +++ b/docs/classes/google-spreadsheet.md @@ -216,6 +216,23 @@ Param|Type|Required|Description - ↩️ **Returns** - Buffer (or stream) containing ODS data +### Developer Metadata + +#### `searchDeveloperMetadata(filters)` (async) :id=fn-searchDeveloperMetadata +> Search for developer metadata entries matching the given filters + +Param|Type|Required|Description +---|---|---|--- +`filters`|Array<[DataFilter](https://developers.google.com/sheets/api/reference/rest/v4/DataFilter)>|✅|Array of DataFilter objects to match against + +- ↩️ **Returns** - `Promise` - array of matching [DeveloperMetadata](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.developerMetadata#DeveloperMetadata) objects + +```javascript +const results = await doc.searchDeveloperMetadata([ + { developerMetadataLookup: { metadataKey: 'my-key' } }, +]); +``` + ### Deletion #### `delete()` (async) :id=fn-delete > delete the document diff --git a/src/lib/GoogleSpreadsheet.ts b/src/lib/GoogleSpreadsheet.ts index 7f81bdd..3bf942b 100644 --- a/src/lib/GoogleSpreadsheet.ts +++ b/src/lib/GoogleSpreadsheet.ts @@ -4,6 +4,8 @@ import { GoogleSpreadsheetWorksheet } from './GoogleSpreadsheetWorksheet'; import { getFieldMask } from './utils'; import { DataFilter, + DataFilterObject, + DeveloperMetadata, GridRange, NamedRangeId, ProtectedRange, @@ -412,15 +414,12 @@ export class GoogleSpreadsheet { async loadCells( /** * single filter or array of filters - * strings are treated as A1 ranges, objects are treated as GridRange objects + * strings are treated as A1 ranges, objects are treated as GridRange objects, + * objects with a `developerMetadataLookup` key are treated as DeveloperMetadataLookup filters * pass nothing to fetch all cells * */ filters?: DataFilter | DataFilter[] ) { - // TODO: make it support DeveloperMetadataLookup objects - - - // TODO: switch to this mode if using a read-only auth token? const readOnlyMode = this.authMode === AUTH_MODES.API_KEY; @@ -433,7 +432,9 @@ export class GoogleSpreadsheet { if (readOnlyMode) { throw new Error('Only A1 ranges are supported when fetching cells with read-only access (using only an API key)'); } - // TODO: make this support Developer Metadata filters + if ('developerMetadataLookup' in filter) { + return { developerMetadataLookup: filter.developerMetadataLookup }; + } return { gridRange: filter }; } throw new Error('Each filter must be an A1 range string or a gridrange object'); @@ -648,6 +649,24 @@ export class GoogleSpreadsheet { await this.driveApi.delete(`permissions/${permissionId}`); } + // DEVELOPER METADATA /////////////////////////////////////////////////////////////////////////// + + /** + * search for developer metadata entries matching the given filters + * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.developerMetadata/search + */ + async searchDeveloperMetadata( + /** array of DataFilter objects to match against */ + filters: DataFilterObject[] + ): Promise { + const response = await this.sheetsApi.post('developerMetadata:search', { + json: { dataFilters: filters }, + }); + const data = await response.json(); + if (!data.matchedDeveloperMetadata) return []; + return data.matchedDeveloperMetadata.map((m: any) => m.developerMetadata); + } + // // CREATE NEW DOC //////////////////////////////////////////////////////////////////////////////// static async createNewSpreadsheetDocument(auth: GoogleApiAuth, properties?: Partial) { diff --git a/src/lib/GoogleSpreadsheetWorksheet.ts b/src/lib/GoogleSpreadsheetWorksheet.ts index 0d2fe49..cfb4433 100644 --- a/src/lib/GoogleSpreadsheetWorksheet.ts +++ b/src/lib/GoogleSpreadsheetWorksheet.ts @@ -250,7 +250,10 @@ export class GoogleSpreadsheetWorksheet { return `${this.a1SheetName}!${filter}`; } if (_.isObject(filter)) { - // TODO: detect and support DeveloperMetadata filters + // pass through developer metadata filters without adding sheetId + if ('developerMetadataLookup' in filter) { + return filter; + } // check if the user passed in a sheet id const filterAny = filter as any; diff --git a/src/lib/types/sheets-types.ts b/src/lib/types/sheets-types.ts index dfaa36c..a9fe521 100644 --- a/src/lib/types/sheets-types.ts +++ b/src/lib/types/sheets-types.ts @@ -410,8 +410,8 @@ export type GridRange = { }; export type GridRangeWithoutWorksheetId = Omit; export type GridRangeWithOptionalWorksheetId = MakeOptional; -export type DataFilter = A1Range | GridRange; -export type DataFilterWithoutWorksheetId = A1Range | GridRangeWithoutWorksheetId; +export type DataFilter = A1Range | GridRange | DataFilterObject; +export type DataFilterWithoutWorksheetId = A1Range | GridRangeWithoutWorksheetId | DataFilterObject; /** * A coordinate in a sheet. All indexes are zero-based. diff --git a/src/test/manage.test.ts b/src/test/manage.test.ts index f079dd9..4feeb79 100644 --- a/src/test/manage.test.ts +++ b/src/test/manage.test.ts @@ -1376,6 +1376,29 @@ describe('Managing doc info and sheets', () => { ); }); + it('can search developer metadata', async () => { + // create a metadata entry to search for + await sheet.createDeveloperMetadata({ + metadataKey: 'search-test-key', + metadataValue: 'search-test-value', + location: { sheetId: sheet.sheetId }, + visibility: 'DOCUMENT', + }); + + const results = await doc.searchDeveloperMetadata([ + { developerMetadataLookup: { metadataKey: 'search-test-key' } }, + ]); + + expect(results).toHaveLength(1); + expect(results[0].metadataKey).toBe('search-test-key'); + expect(results[0].metadataValue).toBe('search-test-value'); + + // clean up + await sheet.deleteDeveloperMetadata({ + developerMetadataLookup: { metadataKey: 'search-test-key' }, + }); + }); + it('can delete developer metadata', async () => { await sheet.deleteDeveloperMetadata({ developerMetadataLookup: { From f2c50d5fbc30733ee3cb3110798de70cf3f132e1 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Fri, 13 Feb 2026 23:19:05 -0800 Subject: [PATCH 2/2] test: add loadCells with developer metadata filter test Tests both doc-level and sheet-level loadCells with a developerMetadataLookup filter on row-level metadata. Co-Authored-By: Claude Opus 4.6 --- src/test/manage.test.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/test/manage.test.ts b/src/test/manage.test.ts index 4feeb79..e7a7850 100644 --- a/src/test/manage.test.ts +++ b/src/test/manage.test.ts @@ -1399,6 +1399,43 @@ describe('Managing doc info and sheets', () => { }); }); + it('can load cells using a developer metadata filter', async () => { + // create row-level metadata on row 0 + await sheet.createDeveloperMetadata({ + metadataKey: 'row-meta-key', + metadataValue: 'row-meta-value', + location: { + dimensionRange: { + sheetId: sheet.sheetId, + dimension: 'ROWS', + startIndex: 0, + endIndex: 1, + }, + }, + visibility: 'DOCUMENT', + }); + + // load cells using developer metadata filter on doc + await doc.loadCells({ + developerMetadataLookup: { metadataKey: 'row-meta-key' }, + }); + + // load cells using developer metadata filter on sheet + sheet.resetLocalCache(true); + await sheet.loadCells({ + developerMetadataLookup: { metadataKey: 'row-meta-key' }, + }); + + // verify cells were loaded (row 0 should be accessible) + const cell = sheet.getCell(0, 0); + expect(cell).toBeTruthy(); + + // clean up + await sheet.deleteDeveloperMetadata({ + developerMetadataLookup: { metadataKey: 'row-meta-key' }, + }); + }); + it('can delete developer metadata', async () => { await sheet.deleteDeveloperMetadata({ developerMetadataLookup: {