UNPKG

@noloco/google-spreadsheet

Version:

Google Sheets API -- simple interface to read/write data and manage sheets

1,065 lines (915 loc) 39.2 kB
import { ReadableStream } from 'node:stream/web'; import * as _ from './lodash'; import { GoogleSpreadsheetRow } from './GoogleSpreadsheetRow'; import { GoogleSpreadsheetCell } from './GoogleSpreadsheetCell'; import { checkForDuplicateHeaders, columnToLetter, getFieldMask, letterToColumn, } from './utils'; import { GoogleSpreadsheet } from './GoogleSpreadsheet'; import { A1Address, A1Range, AddRowOptions, CellDataRange, ColumnIndex, DataFilter, DataFilterWithoutWorksheetId, DataValidationRule, DeveloperMetadataId, DeveloperMetadataKey, DeveloperMetadataValue, DeveloperMetadataVisibility, DimensionRangeIndexes, GetValuesRequestOptions, GridRangeWithOptionalWorksheetId, RowIndex, SpreadsheetId, WorksheetDimension, WorksheetDimensionProperties, WorksheetGridProperties, WorksheetId, WorksheetProperties, } from './types/sheets-types'; // types of cell data accepted when using row based api type RowCellData = string | number | boolean | Date; // raw row data can be passed in as an array or an object using header values as keys type RawRowData = RowCellData[] | Record<string, RowCellData>; export class GoogleSpreadsheetWorksheet { // assume "header row" (for row-based calls) is in first row, can be adjusted later private _headerRowIndex = 1; private _rawProperties: WorksheetProperties | null = null; private _cells: GoogleSpreadsheetCell[][] = []; private _rowMetadata: any[] = []; private _columnMetadata: any[] = []; private _headerValues: string[] | undefined; get headerValues() { if (!this._headerValues) { throw new Error('Header values are not yet loaded'); } return this._headerValues!; } constructor( /** parent GoogleSpreadsheet instance */ readonly _spreadsheet: GoogleSpreadsheet, rawProperties: WorksheetProperties, rawCellData?: CellDataRange[] ) { this._headerRowIndex = 1; // basic properties this._rawProperties = rawProperties; this._cells = []; // we will use a 2d sparse array to store cells; this._rowMetadata = []; // 1d sparse array this._columnMetadata = []; if (rawCellData) this._fillCellData(rawCellData); } // INTERNAL UTILITY FUNCTIONS //////////////////////////////////////////////////////////////////// updateRawData(properties: WorksheetProperties, rawCellData: CellDataRange[]) { this._rawProperties = properties; this._fillCellData(rawCellData); } async _makeSingleUpdateRequest(requestType: string, requestParams: any) { // pass the call up to the parent return this._spreadsheet._makeSingleUpdateRequest(requestType, { ...requestParams, }); } private _ensureInfoLoaded() { if (!this._rawProperties) { throw new Error('You must call `doc.loadInfo()` again before accessing this property'); } } /** clear local cache of sheet data/properties */ resetLocalCache( /** set to true to clear data only, leaving sheet metadata/propeties intact */ dataOnly?: boolean ) { if (!dataOnly) this._rawProperties = null; this._headerValues = undefined; this._headerRowIndex = 1; this._cells = []; } private _fillCellData( dataRanges: CellDataRange[] ) { _.each(dataRanges, (range) => { const startRow = range.startRow || 0; const startColumn = range.startColumn || 0; const numRows = range.rowMetadata.length; const numColumns = range.columnMetadata.length; // update cell data for entire range for (let i = 0; i < numRows; i++) { const actualRow = startRow + i; for (let j = 0; j < numColumns; j++) { const actualColumn = startColumn + j; // if the row has not been initialized yet, do it if (!this._cells[actualRow]) this._cells[actualRow] = []; // see if the response includes some info for the cell const cellData = _.get(range, `rowData[${i}].values[${j}]`); // update the cell object or create it if (this._cells[actualRow][actualColumn]) { this._cells[actualRow][actualColumn]._updateRawData(cellData); } else { this._cells[actualRow][actualColumn] = new GoogleSpreadsheetCell( this, actualRow, actualColumn, cellData ); } } } // update row metadata for (let i = 0; i < range.rowMetadata.length; i++) { this._rowMetadata[startRow + i] = range.rowMetadata[i]; } // update column metadata for (let i = 0; i < range.columnMetadata.length; i++) { this._columnMetadata[startColumn + i] = range.columnMetadata[i]; } }); } // TODO: make this handle A1 ranges as well? private _addSheetIdToRange(range: GridRangeWithOptionalWorksheetId) { if (range.sheetId && range.sheetId !== this.sheetId) { throw new Error('Leave sheet ID blank or set to matching ID of this sheet'); } return { ...range, sheetId: this.sheetId, }; } // PROPERTY GETTERS ////////////////////////////////////////////////////////////////////////////// private _getProp<T extends keyof WorksheetProperties>(param: T): WorksheetProperties[T] { this._ensureInfoLoaded(); // see note about asserting info loaded on GoogleSpreasheet return this._rawProperties![param]; } // eslint-disable-line no-unused-vars private _setProp<T extends keyof WorksheetProperties>(_param: T, _newVal: WorksheetProperties[T]) { throw new Error('Do not update directly - use `updateProperties()`'); } get sheetId() { return this._getProp('sheetId'); } get title() { return this._getProp('title'); } get index() { return this._getProp('index'); } get sheetType() { return this._getProp('sheetType'); } get gridProperties() { return this._getProp('gridProperties'); } get hidden() { return this._getProp('hidden'); } get tabColor() { return this._getProp('tabColor'); } get rightToLeft() { return this._getProp('rightToLeft'); } get rowMetadata(): any[] { return this._rowMetadata; } get columnMetadata(): any[] { return this._columnMetadata; } private get _headerRange() { return `A${this._headerRowIndex}:${this.lastColumnLetter}${this._headerRowIndex}`; } set sheetId(newVal: WorksheetProperties['sheetId']) { this._setProp('sheetId', newVal); } set title(newVal: WorksheetProperties['title']) { this._setProp('title', newVal); } set index(newVal: WorksheetProperties['index']) { this._setProp('index', newVal); } set sheetType(newVal: WorksheetProperties['sheetType']) { this._setProp('sheetType', newVal); } set gridProperties(newVal: WorksheetProperties['gridProperties']) { this._setProp('gridProperties', newVal); } set hidden(newVal: WorksheetProperties['hidden']) { this._setProp('hidden', newVal); } set tabColor(newVal: WorksheetProperties['tabColor']) { this._setProp('tabColor', newVal); } set rightToLeft(newVal: WorksheetProperties['rightToLeft']) { this._setProp('rightToLeft', newVal); } get rowCount() { this._ensureInfoLoaded(); return this.gridProperties.rowCount; } get columnCount() { this._ensureInfoLoaded(); return this.gridProperties.columnCount; } get a1SheetName() { return `'${this.title.replace(/'/g, "''")}'`; } get encodedA1SheetName() { return encodeURIComponent(this.a1SheetName); } get lastColumnLetter() { // TODO: double check behaviour if data not loaded return this.columnCount ? columnToLetter(this.columnCount) : ''; } // CELLS-BASED INTERACTIONS ////////////////////////////////////////////////////////////////////// get cellStats() { let allCells = _.flatten(this._cells); allCells = _.compact(allCells); return { nonEmpty: _.filter(allCells, (c) => c.value).length, loaded: allCells.length, total: this.rowCount * this.columnCount, }; } getCellByA1(a1Address: A1Address) { const split = a1Address.match(/([A-Z]+)([0-9]+)/); if (!split) throw new Error(`Cell address "${a1Address}" not valid`); const columnIndex = letterToColumn(split[1]); const rowIndex = parseInt(split[2]); return this.getCell(rowIndex - 1, columnIndex - 1); } getCell(rowIndex: RowIndex, columnIndex: ColumnIndex) { if (rowIndex < 0 || columnIndex < 0) throw new Error('Min coordinate is 0, 0'); if (rowIndex >= this.rowCount || columnIndex >= this.columnCount) { throw new Error(`Out of bounds, sheet is ${this.rowCount} by ${this.columnCount}`); } if (!_.get(this._cells, `[${rowIndex}][${columnIndex}]`)) { throw new Error('This cell has not been loaded yet'); } return this._cells[rowIndex][columnIndex]; } async loadCells(sheetFilters?: DataFilterWithoutWorksheetId | DataFilterWithoutWorksheetId[]) { // load the whole sheet if (!sheetFilters) return this._spreadsheet.loadCells(this.a1SheetName); const filtersArray = _.isArray(sheetFilters) ? sheetFilters : [sheetFilters]; const filtersArrayWithSheetId: DataFilter[] = _.map(filtersArray, (filter) => { // add sheet name to A1 ranges if (_.isString(filter)) { if (filter.startsWith(this.a1SheetName)) return filter; return `${this.a1SheetName}!${filter}`; } if (_.isObject(filter)) { // TODO: detect and support DeveloperMetadata filters // check if the user passed in a sheet id const filterAny = filter as any; if (filterAny.sheetId && filterAny.sheetId !== this.sheetId) { throw new Error('Leave sheet ID blank or set to matching ID of this sheet'); } return { sheetId: this.sheetId, ...filter }; } throw new Error('Each filter must be a A1 range string or gridrange object'); }); return this._spreadsheet.loadCells(filtersArrayWithSheetId); } async saveUpdatedCells() { const cellsToSave = _.filter(_.flatten(this._cells), { _isDirty: true }); if (cellsToSave.length) { await this.saveCells(cellsToSave); } // TODO: do we want to return stats? or the cells that got updated? } async saveCells(cellsToUpdate: GoogleSpreadsheetCell[]) { // we send an individual "updateCells" request for each cell // because the fields that are udpated for each group are the same // and we dont want to accidentally overwrite something const requests = _.map(cellsToUpdate, (cell) => cell._getUpdateRequest()); const responseRanges = _.map(cellsToUpdate, (c) => `${this.a1SheetName}!${c.a1Address}`); // if nothing is being updated the request returned is just `null` // so we make sure at least 1 request is valid - otherwise google throws a 400 if (!_.compact(requests).length) { throw new Error('At least one cell must have something to update'); } await this._spreadsheet._makeBatchUpdateRequest(requests, responseRanges); } // SAVING THIS FOR FUTURE USE // puts the cells that need updating into batches // async updateCellsByBatches() { // // saving this code, but it's problematic because each group must have the same update fields // const cellsByRow = _.groupBy(cellsToUpdate, 'rowIndex'); // const groupsToSave = []; // _.each(cellsByRow, (cells, rowIndex) => { // let cellGroup = []; // _.each(cells, (c) => { // if (!cellGroup.length) { // cellGroup.push(c); // } else if ( // cellGroup[cellGroup.length - 1].columnIndex === // c.columnIndex - 1 // ) { // cellGroup.push(c); // } else { // groupsToSave.push(cellGroup); // cellGroup = []; // } // }); // groupsToSave.push(cellGroup); // }); // const requests = _.map(groupsToSave, (cellGroup) => ({ // updateCells: { // rows: [ // { // values: _.map(cellGroup, (cell) => ({ // ...cell._draftData.value && { // userEnteredValue: { [cell._draftData.valueType]: cell._draftData.value }, // }, // ...cell._draftData.note !== undefined && { // note: cell._draftData.note , // }, // ...cell._draftData.userEnteredFormat && { // userEnteredValue: cell._draftData.userEnteredFormat, // }, // })), // }, // ], // fields: 'userEnteredValue,note,userEnteredFormat', // start: { // sheetId: this.sheetId, // rowIndex: cellGroup[0].rowIndex, // columnIndex: cellGroup[0].columnIndex, // }, // }, // })); // const responseRanges = _.map(groupsToSave, (cellGroup) => { // let a1Range = cellGroup[0].a1Address; // if (cellGroup.length > 1) // a1Range += `:${cellGroup[cellGroup.length - 1].a1Address}`; // return `${cellGroup[0]._sheet.a1SheetName}!${a1Range}`; // }); // } // ROW BASED FUNCTIONS /////////////////////////////////////////////////////////////////////////// async _ensureHeaderRowLoaded() { if (!this._headerValues) { await this.loadHeaderRow(); } } async loadHeaderRow(headerRowIndex?: number, options?: GetValuesRequestOptions) { if (headerRowIndex !== undefined) this._headerRowIndex = headerRowIndex; const rows = await this.getCellsInRange(this._headerRange, options); this._processHeaderRow(rows); } private _processHeaderRow(rows: any[]) { if (!rows) { throw new Error('No values in the header row - fill the first row with header values before trying to interact with rows'); } this._headerValues = _.map(rows[0], (header) => header?.toString()?.trim()); if (!_.compact(this.headerValues).length) { throw new Error('All your header cells are blank - fill the first row with header values before trying to interact with rows'); } checkForDuplicateHeaders(this.headerValues); } async setHeaderRow(headerValues: string[], headerRowIndex?: number) { if (!headerValues) return; if (headerValues.length > this.columnCount) { throw new Error(`Sheet is not large enough to fit ${headerValues.length} columns. Resize the sheet first.`); } const trimmedHeaderValues = _.map(headerValues, (h) => h?.toString()?.trim()); checkForDuplicateHeaders(trimmedHeaderValues); if (!_.compact(trimmedHeaderValues).length) { throw new Error('All your header cells are blank -'); } if (headerRowIndex) this._headerRowIndex = headerRowIndex; const response = await this._spreadsheet.sheetsApi.request({ method: 'put', url: `/values/${this.encodedA1SheetName}!${this._headerRowIndex}:${this._headerRowIndex}`, params: { valueInputOption: 'USER_ENTERED', // other option is RAW includeValuesInResponse: true, }, data: { range: `${this.a1SheetName}!${this._headerRowIndex}:${this._headerRowIndex}`, majorDimension: 'ROWS', values: [[ ...trimmedHeaderValues, // pad the rest of the row with empty values to clear them all out ..._.times(this.columnCount - trimmedHeaderValues.length, () => ''), ]], }, }); this._headerValues = response.data.updatedData.values[0]; } // TODO: look at these types async addRows( rows: RawRowData[], options: AddRowOptions = {} ) { // adds multiple rows in one API interaction using the append endpoint // each row can be an array or object // an array is just cells // ex: ['column 1', 'column 2', 'column 3'] // an object must use the header row values as keys // ex: { col1: 'column 1', col2: 'column 2', col3: 'column 3' } // google bug that does not handle colons in sheet names // see https://issuetracker.google.com/issues/150373119 if (this.title.includes(':')) { throw new Error('Please remove the ":" from your sheet title. There is a bug with the google API which breaks appending rows if any colons are in the sheet title.'); } if (!_.isArray(rows)) throw new Error('You must pass in an array of row values to append'); await this._ensureHeaderRowLoaded(); // convert each row into an array of cell values rather than the key/value object const rowsAsArrays: RawRowData[] = []; _.each(rows, (row) => { let rowAsArray; if (_.isArray(row)) { rowAsArray = row; } else if (_.isObject(row)) { rowAsArray = []; for (let i = 0; i < this.headerValues.length; i++) { const propName = this.headerValues[i]; rowAsArray[i] = row[propName]; } } else { throw new Error('Each row must be an object or an array'); } rowsAsArrays.push(rowAsArray); }); const response = await this._spreadsheet.sheetsApi.request({ method: 'post', url: `/values/${this.encodedA1SheetName}!A${this._headerRowIndex}:append`, params: { valueInputOption: options.raw ? 'RAW' : 'USER_ENTERED', insertDataOption: options.insert ? 'INSERT_ROWS' : 'OVERWRITE', includeValuesInResponse: true, }, data: { values: rowsAsArrays, }, }); // extract the new row number from the A1-notation data range in the response // ex: in "'Sheet8!A2:C2" -- we want the `2` const { updatedRange } = response.data.updates; let rowNumber = updatedRange.match(/![A-Z]+([0-9]+):?/)[1]; rowNumber = parseInt(rowNumber); this._ensureInfoLoaded(); // if new rows were added, we need update sheet.rowRount if (options.insert) { this._rawProperties!.gridProperties.rowCount += rows.length; } else if (rowNumber + rows.length > this.rowCount) { // have to subtract 1 since one row was inserted at rowNumber this._rawProperties!.gridProperties.rowCount = rowNumber + rows.length - 1; } return _.map(response.data.updates.updatedData.values, (rowValues) => { const row = new GoogleSpreadsheetRow(this, rowNumber++, rowValues); return row; }); } /** add a single row - see addRows for more info */ async addRow(rowValues: RawRowData, options?: AddRowOptions) { const rows = await this.addRows([rowValues], options); return rows[0]; } private _rowCache: GoogleSpreadsheetRow[] = []; async getRows<T extends Record<string, any>>( options?: { /** skip first N rows */ offset?: number, /** limit number of rows fetched */ limit?: number, } & GetValuesRequestOptions ) { // https://developers.google.com/sheets/api/guides/migration // v4 API does not have equivalents for the row-order query parameters provided // Reverse-order is trivial; simply process the returned values array in reverse order. // Order by column is not supported for reads, but it is possible to sort the data then read // v4 API does not currently have a direct equivalent for the Sheets API v3 structured queries // However, you can retrieve the relevant data and sort through it as needed in your application const offset = options?.offset || 0; const limit = options?.limit || this.rowCount - 1; let valueRequestOptions: GetValuesRequestOptions | undefined; if (options?.majorDimension) { valueRequestOptions = { majorDimension: options.majorDimension }; } if (options?.valueRenderOption) { if (!valueRequestOptions) { valueRequestOptions = { valueRenderOption: options.valueRenderOption }; } else { valueRequestOptions.valueRenderOption = options.valueRenderOption; } } const firstRow = 1 + this._headerRowIndex + offset; const lastRow = firstRow + limit - 1; // inclusive so we subtract 1 let rawRows; if (this._headerValues) { const lastColumn = columnToLetter(this.headerValues.length); rawRows = await this.getCellsInRange( `A${firstRow}:${lastColumn}${lastRow}`, valueRequestOptions ); } else { const result = await this.batchGetCellsInRange( [this._headerRange, `A${firstRow}:${this.lastColumnLetter}${lastRow}`], valueRequestOptions ); this._processHeaderRow(result[0]); rawRows = result[1]; } if (!rawRows) return []; const rows = []; let rowNum = firstRow; for (let i = 0; i < rawRows.length; i++) { const row = new GoogleSpreadsheetRow<T>(this, rowNum++, rawRows[i]); this._rowCache[row.rowNumber] = row; rows.push(row); } return rows; } /** * @internal * Used internally to update row numbers after deleting rows. * Should not be called directly. * */ _shiftRowCache(deletedRowNumber: number) { delete this._rowCache[deletedRowNumber]; this._rowCache.forEach((row) => { if (row.rowNumber > deletedRowNumber) { row._updateRowNumber(row.rowNumber - 1); } }); } async clearRows( options?: { start?: number, end?: number, } ) { // default to first row after header const startRowIndex = options?.start || this._headerRowIndex + 1; const endRowIndex = options?.end || this.rowCount; await this._spreadsheet.sheetsApi.post(`/values/${this.encodedA1SheetName}!${startRowIndex}:${endRowIndex}:clear`); this._rowCache.forEach((row) => { if (row.rowNumber >= startRowIndex && row.rowNumber <= endRowIndex) row._clearRowData(); }); } // BASIC PROPS /////////////////////////////////////////////////////////////////////////////////// /** @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest */ async updateProperties(properties: Partial<Omit<WorksheetProperties, 'sheetId'>>) { // Request type = `updateSheetProperties` return this._makeSingleUpdateRequest('updateSheetProperties', { properties: { sheetId: this.sheetId, ...properties, }, fields: getFieldMask(properties), }); } /** * passes through the call to updateProperties to update only the gridProperties object */ async updateGridProperties(gridProperties: WorksheetGridProperties) { return this.updateProperties({ gridProperties }); } /** resize, internally just calls updateGridProperties */ async resize(gridProperties: Pick<WorksheetGridProperties, 'rowCount' | 'columnCount'>) { return this.updateGridProperties(gridProperties); } /** * * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#updatedimensionpropertiesrequest */ async updateDimensionProperties( columnsOrRows: WorksheetDimension, properties: WorksheetDimensionProperties, bounds: Partial<DimensionRangeIndexes> ) { // Request type = `updateDimensionProperties` Object.keys(properties); return this._makeSingleUpdateRequest('updateDimensionProperties', { range: { sheetId: this.sheetId, dimension: columnsOrRows, ...bounds, }, properties, fields: getFieldMask(properties as any), }); } // OTHER ///////////////////////////////////////////////////////////////////////////////////////// // this uses the "values" getter and does not give all the info about the cell contents // it is used internally when loading header cells async getCellsInRange(a1Range: A1Range, options?: GetValuesRequestOptions) { const response = await this._spreadsheet.sheetsApi.get(`/values/${this.encodedA1SheetName}!${a1Range}`, { params: options, }); return response.data.values; } async batchGetCellsInRange(a1Ranges: A1Range[], options?: GetValuesRequestOptions) { const ranges = a1Ranges.map((r) => `ranges=${this.encodedA1SheetName}!${r}`).join('&'); const response = await this._spreadsheet.sheetsApi.get(`/values:batchGet?${ranges}`, { params: options, }); return response.data.valueRanges.map((r: any) => r.values); } async updateNamedRange() { // Request type = `updateNamedRange` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateNamedRangeRequest } async addNamedRange() { // Request type = `addNamedRange` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddNamedRangeRequest } async deleteNamedRange() { // Request type = `deleteNamedRange` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteNamedRangeRequest } async repeatCell() { // Request type = `repeatCell` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#RepeatCellRequest } async autoFill() { // Request type = `autoFill` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AutoFillRequest } async cutPaste() { // Request type = `cutPaste` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#CutPasteRequest } async copyPaste() { // Request type = `copyPaste` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#CopyPasteRequest } // TODO: check types on these ranges /** * Merges all cells in the range * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#MergeCellsRequest */ async mergeCells(range: GridRangeWithOptionalWorksheetId, mergeType = 'MERGE_ALL') { await this._makeSingleUpdateRequest('mergeCells', { mergeType, range: this._addSheetIdToRange(range), }); } /** * Unmerges cells in the given range * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UnmergeCellsRequest */ async unmergeCells(range: GridRangeWithOptionalWorksheetId) { await this._makeSingleUpdateRequest('unmergeCells', { range: this._addSheetIdToRange(range), }); } async updateBorders() { // Request type = `updateBorders` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateBordersRequest } async addFilterView() { // Request type = `addFilterView` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddFilterViewRequest } async appendCells() { // Request type = `appendCells` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AppendCellsRequest } async clearBasicFilter() { // Request type = `clearBasicFilter` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#ClearBasicFilterRequest } async deleteDimension() { // Request type = `deleteDimension` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteDimensionRequest } async deleteEmbeddedObject() { // Request type = `deleteEmbeddedObject` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteEmbeddedObjectRequest } async deleteFilterView() { // Request type = `deleteFilterView` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteFilterViewRequest } async duplicateFilterView() { // Request type = `duplicateFilterView` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DuplicateFilterViewRequest } /** * Duplicate worksheet within the document * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DuplicateSheetRequest */ async duplicate( options?: { id?: WorksheetId, title?: string, index?: number, } ) { const response = await this._makeSingleUpdateRequest('duplicateSheet', { sourceSheetId: this.sheetId, ...options?.index !== undefined && { insertSheetIndex: options.index }, ...options?.id && { newSheetId: options.id }, ...options?.title && { newSheetName: options.title }, }); const newSheetId = response.properties.sheetId; return this._spreadsheet.sheetsById[newSheetId]; } async findReplace() { // Request type = `findReplace` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#FindReplaceRequest } /** * Inserts rows or columns at a particular index * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#InsertDimensionRequest */ async insertDimension( columnsOrRows: WorksheetDimension, rangeIndexes: DimensionRangeIndexes, inheritFromBefore?: boolean ) { if (!columnsOrRows) throw new Error('You need to specify a dimension. i.e. COLUMNS|ROWS'); if (!_.isObject(rangeIndexes)) throw new Error('`range` must be an object containing `startIndex` and `endIndex`'); if (!_.isInteger(rangeIndexes.startIndex) || rangeIndexes.startIndex < 0) throw new Error('range.startIndex must be an integer >=0'); if (!_.isInteger(rangeIndexes.endIndex) || rangeIndexes.endIndex < 0) throw new Error('range.endIndex must be an integer >=0'); if (rangeIndexes.endIndex <= rangeIndexes.startIndex) throw new Error('range.endIndex must be greater than range.startIndex'); // default inheritFromBefore to true - unless inserting in the first row/column if (inheritFromBefore === undefined) { inheritFromBefore = rangeIndexes.startIndex > 0; } // do not allow inheritFromBefore if inserting at first row/column if (inheritFromBefore && rangeIndexes.startIndex === 0) { throw new Error('Cannot set inheritFromBefore to true if inserting in first row/column'); } return this._makeSingleUpdateRequest('insertDimension', { range: { sheetId: this.sheetId, dimension: columnsOrRows, startIndex: rangeIndexes.startIndex, endIndex: rangeIndexes.endIndex, }, inheritFromBefore, }); } async insertRange() { // Request type = `insertRange` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#InsertRangeRequest } async moveDimension() { // Request type = `moveDimension` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#MoveDimensionRequest } async updateEmbeddedObjectPosition() { // Request type = `updateEmbeddedObjectPosition` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateEmbeddedObjectPositionRequest } async pasteData() { // Request type = `pasteData` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#PasteDataRequest } async textToColumns() { // Request type = `textToColumns` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#TextToColumnsRequest } async updateFilterView() { // Request type = `updateFilterView` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateFilterViewRequest } async deleteRange() { // Request type = `deleteRange` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteRangeRequest } async appendDimension() { // Request type = `appendDimension` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AppendDimensionRequest } async addConditionalFormatRule() { // Request type = `addConditionalFormatRule` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddConditionalFormatRuleRequest } async updateConditionalFormatRule() { // Request type = `updateConditionalFormatRule` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateConditionalFormatRuleRequest } async deleteConditionalFormatRule() { // Request type = `deleteConditionalFormatRule` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteConditionalFormatRuleRequest } async sortRange() { // Request type = `sortRange` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#SortRangeRequest } /** * Sets (or unsets) a data validation rule to every cell in the range * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#SetDataValidationRequest */ async setDataValidation( range: GridRangeWithOptionalWorksheetId, /** data validation rule object, or set to false to clear an existing rule */ rule: DataValidationRule | false ) { return this._makeSingleUpdateRequest('setDataValidation', { range: { sheetId: this.sheetId, ...range, }, ...rule && { rule }, }); } async setBasicFilter() { // Request type = `setBasicFilter` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#SetBasicFilterRequest } async addProtectedRange() { // Request type = `addProtectedRange` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddProtectedRangeRequest } async updateProtectedRange() { // Request type = `updateProtectedRange` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateProtectedRangeRequest } async deleteProtectedRange() { // Request type = `deleteProtectedRange` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteProtectedRangeRequest } async autoResizeDimensions() { // Request type = `autoResizeDimensions` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AutoResizeDimensionsRequest } async addChart() { // Request type = `addChart` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddChartRequest } async updateChartSpec() { // Request type = `updateChartSpec` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateChartSpecRequest } async updateBanding() { // Request type = `updateBanding` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateBandingRequest } async addBanding() { // Request type = `addBanding` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddBandingRequest } async deleteBanding() { // Request type = `deleteBanding` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteBandingRequest } async createDeveloperMetadata( metadataKey: DeveloperMetadataKey, metadataValue: DeveloperMetadataValue, visibility: DeveloperMetadataVisibility, metadataId: DeveloperMetadataId ) { // Request type = `createDeveloperMetadata` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#CreateDeveloperMetadataRequest return this._spreadsheet.createSheetDeveloperMetadata( metadataKey, metadataValue, this.sheetId, visibility, metadataId ); } async updateDeveloperMetadata() { // Request type = `updateDeveloperMetadata` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateDeveloperMetadataRequest } async deleteDeveloperMetadata() { // Request type = `deleteDeveloperMetadata` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteDeveloperMetadataRequest } async randomizeRange() { // Request type = `randomizeRange` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#RandomizeRangeRequest } async addDimensionGroup() { // Request type = `addDimensionGroup` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddDimensionGroupRequest } async deleteDimensionGroup() { // Request type = `deleteDimensionGroup` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteDimensionGroupRequest } async updateDimensionGroup() { // Request type = `updateDimensionGroup` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateDimensionGroupRequest } async trimWhitespace() { // Request type = `trimWhitespace` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#TrimWhitespaceRequest } async deleteDuplicates() { // Request type = `deleteDuplicates` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteDuplicatesRequest } async addSlicer() { // Request type = `addSlicer` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddSlicerRequest } async updateSlicerSpec() { // Request type = `updateSlicerSpec` // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSlicerSpecRequest } /** delete this worksheet */ async delete() { return this._spreadsheet.deleteSheet(this.sheetId); } /** * copies this worksheet into another document/spreadsheet * @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.sheets/copyTo * */ async copyToSpreadsheet(destinationSpreadsheetId: SpreadsheetId) { return this._spreadsheet.sheetsApi.post(`/sheets/${this.sheetId}:copyTo`, { destinationSpreadsheetId, }); } /** clear data in the sheet - either the entire sheet or a specific range */ async clear( /** optional A1 range to clear - defaults to entire sheet */ a1Range?: A1Range ) { const range = a1Range ? `!${a1Range}` : ''; // sheet name without ie 'sheet1' rather than 'sheet1'!A1:B5 is all cells await this._spreadsheet.sheetsApi.post(`/values/${this.encodedA1SheetName}${range}:clear`); this.resetLocalCache(true); } /** exports worksheet as CSV file (comma-separated values) */ async downloadAsCSV(): Promise<ArrayBuffer>; async downloadAsCSV(returnStreamInsteadOfBuffer: false): Promise<ArrayBuffer>; async downloadAsCSV(returnStreamInsteadOfBuffer: true): Promise<ReadableStream>; async downloadAsCSV(returnStreamInsteadOfBuffer = false) { return this._spreadsheet._downloadAs('csv', this.sheetId, returnStreamInsteadOfBuffer); } /** exports worksheet as TSC file (tab-separated values) */ async downloadAsTSV(): Promise<ArrayBuffer>; async downloadAsTSV(returnStreamInsteadOfBuffer: false): Promise<ArrayBuffer>; async downloadAsTSV(returnStreamInsteadOfBuffer: true): Promise<ReadableStream>; async downloadAsTSV(returnStreamInsteadOfBuffer = false) { return this._spreadsheet._downloadAs('tsv', this.sheetId, returnStreamInsteadOfBuffer); } /** exports worksheet as PDF */ async downloadAsPDF(): Promise<ArrayBuffer>; async downloadAsPDF(returnStreamInsteadOfBuffer: false): Promise<ArrayBuffer>; async downloadAsPDF(returnStreamInsteadOfBuffer: true): Promise<ReadableStream>; async downloadAsPDF(returnStreamInsteadOfBuffer = false) { return this._spreadsheet._downloadAs('pdf', this.sheetId, returnStreamInsteadOfBuffer); } }