google-spreadsheet
Version:
Google Sheets API -- simple interface to read/write data and manage sheets
1,925 lines • 66.2 kB
JavaScript
import ky, { HTTPError } from "ky";
import { compact, each, filter, find, flatten, get, groupBy, isArray, isBoolean, isEqual, isFinite, isInteger, isNil, isObject, isString, keyBy, keys, map, omit, pickBy, set, some, sortBy, times, unset, values } from "es-toolkit/compat";
//#region src/lib/utils.ts
function getFieldMask(obj) {
let fromGrid = "";
const fromRoot = Object.keys(obj).filter((key) => key !== "gridProperties").join(",");
if (obj.gridProperties) {
fromGrid = Object.keys(obj.gridProperties).map((key) => `gridProperties.${key}`).join(",");
if (fromGrid.length && fromRoot.length) fromGrid = `${fromGrid},`;
}
return fromGrid + fromRoot;
}
function columnToLetter(column) {
let temp;
let letter = "";
let col = column;
while (col > 0) {
temp = (col - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
col = (col - temp - 1) / 26;
}
return letter;
}
function letterToColumn(letter) {
let column = 0;
const { length } = letter;
for (let i = 0; i < length; i++) column += (letter.charCodeAt(i) - 64) * 26 ** (length - i - 1);
return column;
}
function checkForDuplicateHeaders(headers) {
const checkForDupes = groupBy(headers);
each(checkForDupes, (grouped, header) => {
if (!header) return;
if (grouped.length > 1) throw new Error(`Duplicate header detected: "${header}". Please make sure all non-empty headers are unique`);
});
}
//#endregion
//#region src/lib/GoogleSpreadsheetRow.ts
var GoogleSpreadsheetRow = class {
constructor(_worksheet, _rowNumber, _rawData) {
this._worksheet = _worksheet;
this._rowNumber = _rowNumber;
this._rawData = _rawData;
this._padRawData();
}
/** pad _rawData with empty strings so it always matches header length */
_padRawData() {
const headerLength = this._worksheet.headerValues.length;
while (this._rawData.length < headerLength) this._rawData.push("");
}
_deleted = false;
get deleted() {
return this._deleted;
}
/** row number (matches A1 notation, ie first row is 1) */
get rowNumber() {
return this._rowNumber;
}
/**
* @internal
* Used internally to update row numbers after deleting rows.
* Should not be called directly.
*/
_updateRowNumber(newRowNumber) {
this._rowNumber = newRowNumber;
}
/**
* @internal
* Used internally to mark row as deleted.
* Should not be called directly.
*/
_markDeleted() {
this._deleted = true;
}
get a1Range() {
return [
this._worksheet.a1SheetName,
"!",
`A${this._rowNumber}`,
":",
`${columnToLetter(this._worksheet.headerValues.length)}${this._rowNumber}`
].join("");
}
/** get row's value of specific cell (by header key) */
get(key) {
const index = this._worksheet.headerValues.indexOf(key);
return this._rawData[index];
}
/** set row's value of specific cell (by header key) */
set(key, val) {
const index = this._worksheet.headerValues.indexOf(key);
this._rawData[index] = val;
}
/** set multiple values in the row at once from an object */
assign(obj) {
for (const key in obj) this.set(key, obj[key]);
}
/** return raw object of row data */
toObject() {
const o = {};
for (let i = 0; i < this._worksheet.headerValues.length; i++) {
const key = this._worksheet.headerValues[i];
if (!key) continue;
o[key] = this._rawData[i];
}
return o;
}
/** save row values */
async save(options) {
if (this._deleted) throw new Error("This row has been deleted - call getRows again before making updates.");
this._rawData = (await (await this._worksheet._spreadsheet.sheetsApi.put(`values/${encodeURIComponent(this.a1Range)}`, {
searchParams: {
valueInputOption: options?.raw ? "RAW" : "USER_ENTERED",
includeValuesInResponse: true
},
json: {
range: this.a1Range,
majorDimension: "ROWS",
values: [this._rawData]
}
})).json()).updatedData.values?.[0] || [];
this._padRawData();
}
/** delete this row */
async delete() {
if (this._deleted) throw new Error("This row has been deleted - call getRows again before making updates.");
const result = await this._worksheet._makeSingleUpdateRequest("deleteRange", {
range: {
sheetId: this._worksheet.sheetId,
startRowIndex: this._rowNumber - 1,
endRowIndex: this._rowNumber
},
shiftDimension: "ROWS"
});
this._deleted = true;
this._worksheet._shiftRowCache(this.rowNumber);
return result;
}
/**
* @internal
* Used internally to clear row data after calling sheet.clearRows
* Should not be called directly.
*/
_clearRowData() {
for (let i = 0; i < this._rawData.length; i++) this._rawData[i] = "";
}
};
//#endregion
//#region src/lib/GoogleSpreadsheetCellErrorValue.ts
/**
* Cell error
*
* not a js "error" that gets thrown, but a value that holds an error code and message for a cell
* it's useful to use a class so we can check `instanceof`
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ErrorType
*/
var GoogleSpreadsheetCellErrorValue = class {
/**
* type of the error
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ErrorType
* */
type;
/** A message with more information about the error (in the spreadsheet's locale) */
message;
constructor(rawError) {
this.type = rawError.type;
this.message = rawError.message;
}
};
//#endregion
//#region src/lib/GoogleSpreadsheetCell.ts
var GoogleSpreadsheetCell = class {
_rawData;
_draftData = {};
_error;
_deleted = false;
constructor(_sheet, _rowIndex, _columnIndex, rawCellData) {
this._sheet = _sheet;
this._rowIndex = _rowIndex;
this._columnIndex = _columnIndex;
this._updateRawData(rawCellData);
this._rawData = rawCellData;
}
get deleted() {
return this._deleted;
}
/**
* update cell using raw CellData coming back from sheets API
* @internal
*/
_updateRawData(newData) {
this._rawData = newData;
this._draftData = {};
if (this._rawData?.effectiveValue && "errorValue" in this._rawData.effectiveValue) this._error = new GoogleSpreadsheetCellErrorValue(this._rawData.effectiveValue.errorValue);
else this._error = void 0;
}
get rowIndex() {
return this._rowIndex;
}
get columnIndex() {
return this._columnIndex;
}
get a1Column() {
return columnToLetter(this._columnIndex + 1);
}
get a1Row() {
return this._rowIndex + 1;
}
get a1Address() {
return `${this.a1Column}${this.a1Row}`;
}
/**
* @internal
* Used internally to update cell indices after deleting rows/columns.
* Should not be called directly.
*/
_updateIndices(rowIndex, columnIndex) {
this._rowIndex = rowIndex;
this._columnIndex = columnIndex;
}
/**
* @internal
* Used internally to mark cell as deleted.
* Should not be called directly.
*/
_markDeleted() {
this._deleted = true;
}
get value() {
if (this._draftData.value !== void 0) throw new Error("Value has been changed");
if (this._error) return this._error;
if (!this._rawData?.effectiveValue) return null;
return values(this._rawData.effectiveValue)[0];
}
set value(newValue) {
if (this._deleted) throw new Error("This cell has been deleted - reload cells before making updates.");
if (newValue instanceof GoogleSpreadsheetCellErrorValue) throw new Error("You can't manually set a value to an error");
if (isBoolean(newValue)) this._draftData.valueType = "boolValue";
else if (isString(newValue)) if (newValue.substring(0, 1) === "=") this._draftData.valueType = "formulaValue";
else this._draftData.valueType = "stringValue";
else if (isFinite(newValue)) this._draftData.valueType = "numberValue";
else if (isNil(newValue)) {
this._draftData.valueType = "stringValue";
newValue = "";
} else throw new Error("Set value to boolean, string, or number");
this._draftData.value = newValue;
}
get valueType() {
if (this._error) return "errorValue";
if (!this._rawData?.effectiveValue) return null;
return keys(this._rawData.effectiveValue)[0];
}
/** The formatted value of the cell - this is the value as it's shown to the user */
get formattedValue() {
return this._rawData?.formattedValue || null;
}
get formula() {
return get(this._rawData, "userEnteredValue.formulaValue", null);
}
set formula(newValue) {
if (!newValue) throw new Error("To clear a formula, set `cell.value = null`");
if (newValue.substring(0, 1) !== "=") throw new Error("formula must begin with \"=\"");
this.value = newValue;
}
/**
* @deprecated use `cell.errorValue` instead
*/
get formulaError() {
return this._error;
}
/**
* error contained in the cell, which can happen with a bad formula (maybe some other weird cases?)
*/
get errorValue() {
return this._error;
}
get numberValue() {
if (this.valueType !== "numberValue") return void 0;
return this.value;
}
set numberValue(val) {
this.value = val;
}
get boolValue() {
if (this.valueType !== "boolValue") return void 0;
return this.value;
}
set boolValue(val) {
this.value = val;
}
get stringValue() {
if (this.valueType !== "stringValue") return void 0;
return this.value;
}
set stringValue(val) {
this._draftData.valueType = "stringValue";
this._draftData.value = val || "";
}
/**
* Hyperlink contained within the cell.
*
* To modify, do not set directly. Instead set cell.formula, for example `cell.formula = \'=HYPERLINK("http://google.com", "Google")\'`
*/
get hyperlink() {
if (this._draftData.value) throw new Error("Save cell to be able to read hyperlink");
return this._rawData?.hyperlink;
}
/** a note attached to the cell */
get note() {
return this._draftData.note !== void 0 ? this._draftData.note : this._rawData?.note || "";
}
set note(newVal) {
if (newVal === null || newVal === void 0 || newVal === false) newVal = "";
if (!isString(newVal)) throw new Error("Note must be a string");
if (newVal === this._rawData?.note) delete this._draftData.note;
else this._draftData.note = newVal;
}
get userEnteredFormat() {
return Object.freeze(this._rawData?.userEnteredFormat);
}
get effectiveFormat() {
return Object.freeze(this._rawData?.effectiveFormat);
}
_getFormatParam(param) {
if (get(this._draftData, `userEnteredFormat.${param}`)) throw new Error("User format is unsaved - save the cell to be able to read it again");
return Object.freeze(this._rawData.userEnteredFormat[param]);
}
_setFormatParam(param, newVal) {
if (isEqual(newVal, get(this._rawData, `userEnteredFormat.${param}`))) unset(this._draftData, `userEnteredFormat.${param}`);
else {
set(this._draftData, `userEnteredFormat.${param}`, newVal);
this._draftData.clearFormat = false;
}
}
get numberFormat() {
return this._getFormatParam("numberFormat");
}
get backgroundColor() {
return this._getFormatParam("backgroundColor");
}
get backgroundColorStyle() {
return this._getFormatParam("backgroundColorStyle");
}
get borders() {
return this._getFormatParam("borders");
}
get padding() {
return this._getFormatParam("padding");
}
get horizontalAlignment() {
return this._getFormatParam("horizontalAlignment");
}
get verticalAlignment() {
return this._getFormatParam("verticalAlignment");
}
get wrapStrategy() {
return this._getFormatParam("wrapStrategy");
}
get textDirection() {
return this._getFormatParam("textDirection");
}
get textFormat() {
return this._getFormatParam("textFormat");
}
get hyperlinkDisplayType() {
return this._getFormatParam("hyperlinkDisplayType");
}
get textRotation() {
return this._getFormatParam("textRotation");
}
set numberFormat(newVal) {
this._setFormatParam("numberFormat", newVal);
}
set backgroundColor(newVal) {
this._setFormatParam("backgroundColor", newVal);
}
set backgroundColorStyle(newVal) {
this._setFormatParam("backgroundColorStyle", newVal);
}
set borders(newVal) {
this._setFormatParam("borders", newVal);
}
set padding(newVal) {
this._setFormatParam("padding", newVal);
}
set horizontalAlignment(newVal) {
this._setFormatParam("horizontalAlignment", newVal);
}
set verticalAlignment(newVal) {
this._setFormatParam("verticalAlignment", newVal);
}
set wrapStrategy(newVal) {
this._setFormatParam("wrapStrategy", newVal);
}
set textDirection(newVal) {
this._setFormatParam("textDirection", newVal);
}
set textFormat(newVal) {
this._setFormatParam("textFormat", newVal);
}
set hyperlinkDisplayType(newVal) {
this._setFormatParam("hyperlinkDisplayType", newVal);
}
set textRotation(newVal) {
this._setFormatParam("textRotation", newVal);
}
clearAllFormatting() {
this._draftData.clearFormat = true;
delete this._draftData.userEnteredFormat;
}
get _isDirty() {
if (this._draftData.note !== void 0) return true;
if (keys(this._draftData.userEnteredFormat).length) return true;
if (this._draftData.clearFormat) return true;
if (this._draftData.value !== void 0) return true;
return false;
}
discardUnsavedChanges() {
this._draftData = {};
}
/**
* saves updates for single cell
* usually it's better to make changes and call sheet.saveUpdatedCells
* */
async save() {
await this._sheet.saveCells([this]);
}
/**
* used by worksheet when saving cells
* returns an individual batchUpdate request to update the cell
* @internal
*/
_getUpdateRequest() {
const isValueUpdated = this._draftData.value !== void 0;
const isNoteUpdated = this._draftData.note !== void 0;
const isFormatUpdated = !!keys(this._draftData.userEnteredFormat || {}).length;
const isFormatCleared = this._draftData.clearFormat;
if (!some([
isValueUpdated,
isNoteUpdated,
isFormatUpdated,
isFormatCleared
])) return null;
const format = {
...this._rawData?.userEnteredFormat,
...this._draftData.userEnteredFormat
};
if (get(this._draftData, "userEnteredFormat.backgroundColor")) delete format.backgroundColorStyle;
return { updateCells: {
rows: [{ values: [{
...isValueUpdated && { userEnteredValue: { [this._draftData.valueType]: this._draftData.value } },
...isNoteUpdated && { note: this._draftData.note },
...isFormatUpdated && { userEnteredFormat: format },
...isFormatCleared && { userEnteredFormat: {} }
}] }],
fields: keys(pickBy({
userEnteredValue: isValueUpdated,
note: isNoteUpdated,
userEnteredFormat: isFormatUpdated || isFormatCleared
})).join(","),
start: {
sheetId: this._sheet.sheetId,
rowIndex: this.rowIndex,
columnIndex: this.columnIndex
}
} };
}
};
//#endregion
//#region src/lib/GoogleSpreadsheetWorksheet.ts
var GoogleSpreadsheetWorksheet = class {
_headerRowIndex = 1;
_rawProperties = null;
_cells = [];
_rowMetadata = [];
_columnMetadata = [];
_protectedRanges = null;
_headerValues;
get headerValues() {
if (!this._headerValues) throw new Error("Header values are not yet loaded");
return this._headerValues;
}
constructor(_spreadsheet, rawProperties, rawCellData, protectedRanges) {
this._spreadsheet = _spreadsheet;
this._headerRowIndex = 1;
this._rawProperties = rawProperties;
this._cells = [];
this._rowMetadata = [];
this._columnMetadata = [];
if (protectedRanges) this._protectedRanges = protectedRanges;
if (rawCellData) this._fillCellData(rawCellData);
}
updateRawData(properties, rawCellData, protectedRanges) {
this._rawProperties = properties;
this._fillCellData(rawCellData);
if (protectedRanges) this._protectedRanges = protectedRanges;
}
async _makeSingleUpdateRequest(requestType, requestParams) {
return this._spreadsheet._makeSingleUpdateRequest(requestType, { ...requestParams });
}
_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(dataOnly) {
if (!dataOnly) this._rawProperties = null;
this._headerValues = void 0;
this._headerRowIndex = 1;
this._cells = [];
}
_fillCellData(dataRanges) {
each(dataRanges, (range) => {
const startRow = range.startRow || 0;
const startColumn = range.startColumn || 0;
const numRows = range.rowMetadata.length;
const numColumns = range.columnMetadata.length;
for (let i = 0; i < numRows; i++) {
const actualRow = startRow + i;
for (let j = 0; j < numColumns; j++) {
const actualColumn = startColumn + j;
if (!this._cells[actualRow]) this._cells[actualRow] = [];
const cellData = get(range, `rowData[${i}].values[${j}]`);
if (this._cells[actualRow][actualColumn]) this._cells[actualRow][actualColumn]._updateRawData(cellData);
else this._cells[actualRow][actualColumn] = new GoogleSpreadsheetCell(this, actualRow, actualColumn, cellData);
}
}
for (let i = 0; i < range.rowMetadata.length; i++) this._rowMetadata[startRow + i] = range.rowMetadata[i];
for (let i = 0; i < range.columnMetadata.length; i++) this._columnMetadata[startColumn + i] = range.columnMetadata[i];
});
}
_addSheetIdToRange(range) {
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
};
}
_getProp(param) {
this._ensureInfoLoaded();
return this._rawProperties[param];
}
_setProp(_param, _newVal) {
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 protectedRanges() {
return this._protectedRanges;
}
get _headerRange() {
return `A${this._headerRowIndex}:${this.lastColumnLetter}${this._headerRowIndex}`;
}
set sheetId(newVal) {
this._setProp("sheetId", newVal);
}
set title(newVal) {
this._setProp("title", newVal);
}
set index(newVal) {
this._setProp("index", newVal);
}
set sheetType(newVal) {
this._setProp("sheetType", newVal);
}
set gridProperties(newVal) {
this._setProp("gridProperties", newVal);
}
set hidden(newVal) {
this._setProp("hidden", newVal);
}
set tabColor(newVal) {
this._setProp("tabColor", newVal);
}
set rightToLeft(newVal) {
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() {
return this.columnCount ? columnToLetter(this.columnCount) : "";
}
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) {
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, 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) {
if (!sheetFilters) return this._spreadsheet.loadCells(this.a1SheetName);
const filtersArray = isArray(sheetFilters) ? sheetFilters : [sheetFilters];
const filtersArrayWithSheetId = map(filtersArray, (filter) => {
if (isString(filter)) {
if (filter.startsWith(this.a1SheetName)) return filter;
return `${this.a1SheetName}!${filter}`;
}
if (isObject(filter)) {
if ("developerMetadataLookup" in filter) return filter;
const filterAny = filter;
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);
}
async saveCells(cellsToUpdate) {
const requests = map(cellsToUpdate, (cell) => cell._getUpdateRequest());
const responseRanges = map(cellsToUpdate, (c) => `${this.a1SheetName}!${c.a1Address}`);
if (!compact(requests).length) throw new Error("At least one cell must have something to update");
await this._spreadsheet._makeBatchUpdateRequest(requests, responseRanges);
}
async _ensureHeaderRowLoaded() {
if (!this._headerValues) await this.loadHeaderRow();
}
async loadHeaderRow(headerRowIndex) {
if (headerRowIndex !== void 0) this._headerRowIndex = headerRowIndex;
const rows = await this.getCellsInRange(this._headerRange);
this._processHeaderRow(rows);
}
_processHeaderRow(rows) {
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?.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, headerRowIndex) {
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?.trim());
checkForDuplicateHeaders(trimmedHeaderValues);
if (!compact(trimmedHeaderValues).length) throw new Error("All your header cells are blank -");
if (headerRowIndex) this._headerRowIndex = headerRowIndex;
this._headerValues = (await (await this._spreadsheet.sheetsApi.put(`values/${this.encodedA1SheetName}!${this._headerRowIndex}:${this._headerRowIndex}`, {
searchParams: {
valueInputOption: "USER_ENTERED",
includeValuesInResponse: true
},
json: {
range: `${this.a1SheetName}!${this._headerRowIndex}:${this._headerRowIndex}`,
majorDimension: "ROWS",
values: [[...trimmedHeaderValues, ...times(this.columnCount - trimmedHeaderValues.length, () => "")]]
}
})).json()).updatedData.values[0];
}
async addRows(rows, options = {}) {
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();
const rowsAsArrays = [];
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 data = await (await this._spreadsheet.sheetsApi.post(`values/${this.encodedA1SheetName}!A${this._headerRowIndex}:append`, {
searchParams: {
valueInputOption: options.raw ? "RAW" : "USER_ENTERED",
insertDataOption: options.insert ? "INSERT_ROWS" : "OVERWRITE",
includeValuesInResponse: true
},
json: { values: rowsAsArrays }
})).json();
const { updatedRange } = data.updates;
let rowNumber = updatedRange.match(/![A-Z]+([0-9]+):?/)[1];
rowNumber = parseInt(rowNumber);
this._ensureInfoLoaded();
if (options.insert) this._rawProperties.gridProperties.rowCount += rows.length;
else if (rowNumber + rows.length > this.rowCount) this._rawProperties.gridProperties.rowCount = rowNumber + rows.length - 1;
return map(data.updates.updatedData.values, (rowValues) => {
return new GoogleSpreadsheetRow(this, rowNumber++, rowValues);
});
}
/**
* add a single row - see addRows for more info
*/
async addRow(rowValues, options) {
return (await this.addRows([rowValues], options))[0];
}
_rowCache = [];
async getRows(options) {
const offset = options?.offset || 0;
const limit = options?.limit || this.rowCount - 1;
const firstRow = 1 + this._headerRowIndex + offset;
const lastRow = firstRow + limit - 1;
let rawRows;
if (this._headerValues) {
const lastColumn = columnToLetter(this.headerValues.length);
rawRows = await this.getCellsInRange(`A${firstRow}:${lastColumn}${lastRow}`);
} else {
const result = await this.batchGetCellsInRange([this._headerRange, `A${firstRow}:${this.lastColumnLetter}${lastRow}`]);
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(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) {
delete this._rowCache[deletedRowNumber];
this._rowCache.forEach((row) => {
if (row.rowNumber > deletedRowNumber) row._updateRowNumber(row.rowNumber - 1);
});
}
/**
* @internal
* Used internally to update row numbers after deleting multiple rows.
* Should not be called directly.
* */
_shiftRowCacheBulk(startIndex, endIndex) {
const numDeleted = endIndex - startIndex;
const startRow = startIndex + 1;
const endRow = endIndex;
for (let rowNum = startRow; rowNum <= endRow; rowNum++) {
const row = this._rowCache[rowNum];
if (row) row._markDeleted();
delete this._rowCache[rowNum];
}
this._rowCache.forEach((row) => {
if (row.rowNumber > endRow) row._updateRowNumber(row.rowNumber - numDeleted);
});
}
/**
* @internal
* Used internally to shift cell cache after deleting rows.
* Should not be called directly.
* */
_shiftCellCacheRows(startIndex, endIndex) {
const numDeleted = endIndex - startIndex;
for (let rowIndex = startIndex; rowIndex < endIndex; rowIndex++) {
const row = this._cells[rowIndex];
if (row) row.forEach((cell) => {
if (cell) cell._markDeleted();
});
delete this._cells[rowIndex];
}
const rowsToShift = [];
for (let rowIndex = endIndex; rowIndex < this._cells.length; rowIndex++) if (this._cells[rowIndex]) rowsToShift.push({
oldRowIndex: rowIndex,
cells: this._cells[rowIndex]
});
rowsToShift.forEach(({ oldRowIndex, cells }) => {
delete this._cells[oldRowIndex];
const newRowIndex = oldRowIndex - numDeleted;
this._cells[newRowIndex] = cells;
cells.forEach((cell, colIndex) => {
if (cell) cell._updateIndices(newRowIndex, colIndex);
});
});
}
/**
* @internal
* Used internally to shift cell cache after deleting columns.
* Should not be called directly.
* */
_shiftCellCacheColumns(startIndex, endIndex) {
const numDeleted = endIndex - startIndex;
this._cells.forEach((row, rowIndex) => {
if (!row) return;
for (let colIndex = startIndex; colIndex < endIndex; colIndex++) {
const cell = row[colIndex];
if (cell) cell._markDeleted();
delete row[colIndex];
}
const cellsToShift = [];
for (let colIndex = endIndex; colIndex < row.length; colIndex++) if (row[colIndex]) cellsToShift.push({
oldColIndex: colIndex,
cell: row[colIndex]
});
cellsToShift.forEach(({ oldColIndex, cell }) => {
delete row[oldColIndex];
const newColIndex = oldColIndex - numDeleted;
row[newColIndex] = cell;
cell._updateIndices(rowIndex, newColIndex);
});
});
}
async clearRows(options) {
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();
});
}
/** @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateSheetPropertiesRequest */
async updateProperties(properties) {
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) {
return this.updateProperties({ gridProperties });
}
/**
* resize, internally just calls updateGridProperties
*/
async resize(gridProperties) {
return this.updateGridProperties(gridProperties);
}
/**
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#updatedimensionpropertiesrequest
*/
async updateDimensionProperties(columnsOrRows, properties, bounds) {
return this._makeSingleUpdateRequest("updateDimensionProperties", {
range: {
sheetId: this.sheetId,
dimension: columnsOrRows,
...bounds
},
properties,
fields: getFieldMask(properties)
});
}
async getCellsInRange(a1Range, options) {
return (await (await this._spreadsheet.sheetsApi.get(`values/${this.encodedA1SheetName}!${a1Range}`, { searchParams: options })).json()).values;
}
async batchGetCellsInRange(a1Ranges, options) {
const ranges = a1Ranges.map((r) => `ranges=${this.encodedA1SheetName}!${r}`).join("&");
return (await (await this._spreadsheet.sheetsApi.get(`values:batchGet?${ranges}`, { searchParams: options })).json()).valueRanges.map((r) => r.values);
}
/**
* Updates an existing named range
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateNamedRangeRequest
*/
async updateNamedRange(namedRangeId, namedRange, fields) {
return this._makeSingleUpdateRequest("updateNamedRange", {
namedRange: {
namedRangeId,
...namedRange.name && { name: namedRange.name },
...namedRange.range && { range: this._addSheetIdToRange(namedRange.range) }
},
fields
});
}
/**
* Creates a new named range in this worksheet (convenience method that auto-fills sheetId)
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddNamedRangeRequest
*/
async addNamedRange(name, range, namedRangeId) {
return this._spreadsheet.addNamedRange(name, this._addSheetIdToRange(range), namedRangeId);
}
/**
* Deletes a named range (convenience wrapper)
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteNamedRangeRequest
*/
async deleteNamedRange(namedRangeId) {
return this._spreadsheet.deleteNamedRange(namedRangeId);
}
/**
* Updates all cells in a range with the same cell data
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#RepeatCellRequest
*/
async repeatCell(range, cell, fields) {
await this._makeSingleUpdateRequest("repeatCell", {
range: this._addSheetIdToRange(range),
cell,
fields
});
}
/**
* Auto-fills cells with data following a pattern (like dragging the fill handle)
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AutoFillRequest
*/
async autoFill(rangeOrSource, useAlternateSeries) {
const isSourceAndDestination = "dimension" in rangeOrSource;
await this._makeSingleUpdateRequest("autoFill", {
...isSourceAndDestination ? { sourceAndDestination: {
...rangeOrSource,
source: this._addSheetIdToRange(rangeOrSource.source)
} } : { range: this._addSheetIdToRange(rangeOrSource) },
...useAlternateSeries !== void 0 && { useAlternateSeries }
});
}
/**
* Cuts data from a source range and pastes it to a destination coordinate
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#CutPasteRequest
*/
async cutPaste(source, destination, pasteType = "PASTE_NORMAL") {
await this._makeSingleUpdateRequest("cutPaste", {
source: this._addSheetIdToRange(source),
destination: {
sheetId: this.sheetId,
rowIndex: destination.rowIndex,
columnIndex: destination.columnIndex
},
pasteType
});
}
/**
* Copies data from a source range and pastes it to a destination range
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#CopyPasteRequest
*/
async copyPaste(source, destination, pasteType = "PASTE_NORMAL", pasteOrientation = "NORMAL") {
await this._makeSingleUpdateRequest("copyPaste", {
source: this._addSheetIdToRange(source),
destination: this._addSheetIdToRange(destination),
pasteType,
pasteOrientation
});
}
/**
* Merges all cells in the range
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#MergeCellsRequest
*/
async mergeCells(range, 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) {
await this._makeSingleUpdateRequest("unmergeCells", { range: this._addSheetIdToRange(range) });
}
/**
* Updates borders for a range
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateBordersRequest
*/
async updateBorders(range, borders) {
await this._makeSingleUpdateRequest("updateBorders", {
range: this._addSheetIdToRange(range),
...borders
});
}
/**
* Adds a filter view to the sheet
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddFilterViewRequest
*/
async addFilterView(filter) {
return this._makeSingleUpdateRequest("addFilterView", { filter });
}
/**
* Appends cells after the last row with data in a sheet
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AppendCellsRequest
*/
async appendCells(rows, fields) {
await this._makeSingleUpdateRequest("appendCells", {
sheetId: this.sheetId,
rows,
fields
});
}
/**
* Clears the basic filter on this sheet
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#ClearBasicFilterRequest
*/
async clearBasicFilter() {
await this._makeSingleUpdateRequest("clearBasicFilter", { sheetId: this.sheetId });
}
/**
* Delete rows or columns in a given range
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteDimensionRequest
*/
async deleteDimension(columnsOrRows, rangeIndexes) {
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");
const result = await this._makeSingleUpdateRequest("deleteDimension", { range: {
sheetId: this.sheetId,
dimension: columnsOrRows,
startIndex: rangeIndexes.startIndex,
endIndex: rangeIndexes.endIndex
} });
if (columnsOrRows === "ROWS") {
this._shiftRowCacheBulk(rangeIndexes.startIndex, rangeIndexes.endIndex);
this._shiftCellCacheRows(rangeIndexes.startIndex, rangeIndexes.endIndex);
} else this._shiftCellCacheColumns(rangeIndexes.startIndex, rangeIndexes.endIndex);
return result;
}
/**
* Delete rows by index
*/
async deleteRows(startIndex, endIndex) {
return this.deleteDimension("ROWS", {
startIndex,
endIndex
});
}
/**
* Delete columns by index
*/
async deleteColumns(startIndex, endIndex) {
return this.deleteDimension("COLUMNS", {
startIndex,
endIndex
});
}
async deleteEmbeddedObject() {
throw new Error("Not implemented yet");
}
/**
* Deletes a filter view from the sheet
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteFilterViewRequest
*/
async deleteFilterView(filterId) {
await this._makeSingleUpdateRequest("deleteFilterView", { filterId });
}
/**
* Duplicates a filter view
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DuplicateFilterViewRequest
*/
async duplicateFilterView(filterId) {
await this._makeSingleUpdateRequest("duplicateFilterView", { filterId });
}
/**
* Duplicate worksheet within the document
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DuplicateSheetRequest
*/
async duplicate(options) {
const newSheetId = (await this._makeSingleUpdateRequest("duplicateSheet", {
sourceSheetId: this.sheetId,
...options?.index !== void 0 && { insertSheetIndex: options.index },
...options?.id && { newSheetId: options.id },
...options?.title && { newSheetName: options.title }
})).properties.sheetId;
return this._spreadsheet.sheetsById[newSheetId];
}
/**
* Finds and replaces text in cells
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#FindReplaceRequest
*/
async findReplace(find, replacement, options, range) {
await this._makeSingleUpdateRequest("findReplace", {
find,
replacement,
...options,
...range ? { range: this._addSheetIdToRange(range) } : { sheetId: this.sheetId }
});
}
/**
* Inserts rows or columns at a particular index
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#InsertDimensionRequest
*/
async insertDimension(columnsOrRows, rangeIndexes, inheritFromBefore) {
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");
if (inheritFromBefore === void 0) inheritFromBefore = rangeIndexes.startIndex > 0;
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
});
}
/**
* insert empty cells in a range, shifting existing cells in the specified direction
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#InsertRangeRequest
*/
async insertRange(range, shiftDimension) {
await this._makeSingleUpdateRequest("insertRange", {
range: this._addSheetIdToRange(range),
shiftDimension
});
}
/**
* Moves rows or columns to a different position within the sheet
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#MoveDimensionRequest
*/
async moveDimension(dimension, source, destinationIndex) {
await this._makeSingleUpdateRequest("moveDimension", {
source: {
sheetId: this.sheetId,
dimension,
startIndex: source.startIndex,
endIndex: source.endIndex
},
destinationIndex
});
}
async updateEmbeddedObjectPosition() {
throw new Error("Not implemented yet");
}
/**
* Inserts data into the spreadsheet starting at the specified coordinate
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#PasteDataRequest
*/
async pasteData(coordinate, data, delimiter, type = "PASTE_NORMAL") {
await this._makeSingleUpdateRequest("pasteData", {
coordinate: {
sheetId: this.sheetId,
rowIndex: coordinate.rowIndex,
columnIndex: coordinate.columnIndex
},
data,
delimiter,
type
});
}
/**
* Splits a column of text into multiple columns based on a delimiter
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#TextToColumnsRequest
*/
async textToColumns(source, delimiterType, delimiter) {
await this._makeSingleUpdateRequest("textToColumns", {
source: this._addSheetIdToRange(source),
delimiterType,
...delimiter && { delimiter }
});
}
/**
* Updates properties of a filter view
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateFilterViewRequest
*/
async updateFilterView(filter, fields) {
await this._makeSingleUpdateRequest("updateFilterView", {
filter,
fields
});
}
/**
* Deletes a range of cells and shifts remaining cells
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteRangeRequest
*/
async deleteRange(range, shiftDimension) {
await this._makeSingleUpdateRequest("deleteRange", {
range: this._addSheetIdToRange(range),
shiftDimension
});
}
/**
* Appends rows or columns to the end of a sheet
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AppendDimensionRequest
*/
async appendDimension(dimension, length) {
await this._makeSingleUpdateRequest("appendDimension", {
sheetId: this.sheetId,
dimension,
length
});
}
/**
* Adds a new conditional formatting rule at the given index
* All subsequent rules' indexes are incremented
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddConditionalFormatRuleRequest
*/
async addConditionalFormatRule(rule, index) {
await this._makeSingleUpdateRequest("addConditionalFormatRule", {
rule,
index
});
}
/**
* Updates a conditional format rule at the given index, or moves a conditional format rule to another index
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateConditionalFormatRuleRequest
*/
async updateConditionalFormatRule(options) {
await this._makeSingleUpdateRequest("updateConditionalFormatRule", options);
}
/**
* Deletes a conditional format rule at the given index
* All subsequent rules' indexes are decremented
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteConditionalFormatRuleRequest
*/
async deleteConditionalFormatRule(index, sheetId) {
await this._makeSingleUpdateRequest("deleteConditionalFormatRule", {
index,
sheetId: sheetId ?? this.sheetId
});
}
/**
* Sorts data in rows based on sort order per column
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#SortRangeRequest
*/
async sortRange(range, sortSpecs) {
await this._makeSingleUpdateRequest("sortRange", {
range: this._addSheetIdToRange(range),
sortSpecs
});
}
/**
* 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, rule) {
return this._makeSingleUpdateRequest("setDataValidation", {
range: {
sheetId: this.sheetId,
...range
},
...rule && { rule }
});
}
/**
* Sets the basic filter on this sheet
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#SetBasicFilterRequest
*/
async setBasicFilter(filter) {
await this._makeSingleUpdateRequest("setBasicFilter", { filter: {
...filter,
...filter.range && { range: this._addSheetIdToRange(filter.range) }
} });
}
/**
* add a new protected range to the sheet
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddProtectedRangeRequest
*/
async addProtectedRange(protectedRange) {
if (!protectedRange.range && !protectedRange.namedRangeId) throw new Error("No range specified: either range or namedRangeId is required");
return this._makeSingleUpdateRequest("addProtectedRange", { protectedRange });
}
/**
* update an existing protected range
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateProtectedRangeRequest
*/
async updateProtectedRange(protectedRangeId, protectedRange) {
return this._makeSingleUpdateRequest("updateProtectedRange", {
protectedRange: {
protectedRangeId,
...protectedRange
},
fields: getFieldMask(protectedRange)
});
}
/**
* delete a protected range by ID
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteProtectedRangeRequest
*/
async deleteProtectedRange(protectedRangeId) {
return this._makeSingleUpdateRequest("deleteProtectedRange", { protectedRangeId });
}
/**
* auto-resize rows or columns to fit their contents
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AutoResizeDimensionsRequest
*/
async autoResizeDimensions(columnsOrRows, rangeIndexes) {
return this._makeSingleUpdateRequest("autoResizeDimensions", { dimensions: {
sheetId: this.sheetId,
dimension: columnsOrRows,
...rangeIndexes
} });
}
async addChart() {
throw new Error("Not implemented yet");
}
async updateChartSpec() {
throw new Error("Not implemented yet");
}
/**
* Updates properties of a banded range
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateBandingRequest
*/
async updateBanding(bandedRange, fields) {
await this._makeSingleUpdateRequest("updateBanding", {
bandedRange,
fields
});
}
/**
* Adds a new banded range to the sheet
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddBandingRequest
*/
async addBanding(bandedRange) {
return this._makeSingleUpdateRequest("addBanding", { bandedRange });
}
/**
* Deletes a banded range from the sheet
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteBandingRequest
*/
async deleteBanding(bandedRangeId) {
await this._makeSingleUpdateRequest("deleteBanding", { bandedRangeId });
}
/**
* Creates developer metadata
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#CreateDeveloperMetadataRequest
*/
async createDeveloperMetadata(developerMetadata) {
return this._makeSingleUpdateRequest("createDeveloperMetadata", { developerMetadata });
}
/**
* Updates developer metadata that matches the specified filters
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateDeveloperMetadataRequest
*/
async updateDeveloperMetadata(dataFilters, developerMetadata, fields) {
await this._makeSingleUpdateRequest("updateDeveloperMetadata", {
dataFilters,
developerMetadata,
fields
});
}
/**
* Deletes developer metadata that matches the specified filter
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteDeveloperMetadataRequest
*/
async deleteDeveloperMetadata(dataFilter) {
await this._makeSingleUpdateRequest("deleteDeveloperMetadata", { dataFilter });
}
/**
* Randomizes the order of rows in a range
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#RandomizeRangeRequest
*/
async randomizeRange(range) {
await this._makeSingleUpdateRequest("randomizeRange", { range: this._addSheetIdToRange(range) });
}
async addDimensionGroup() {
throw new Error("Not implemented yet");
}
async deleteDimensionGroup() {
throw new Error("Not implemented yet");
}
async updateDimensionGroup() {
throw new Error("Not implemented yet");
}
/**
* Trims whitespace from the start and end of each cell's text
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#TrimWhitespaceRequest
*/
async trimWhitespace(range) {
await this._makeSingleUpdateRequest("trimWhitespace", { range: this._addSheetIdToRange(range) });
}
/**
* Removes duplicate rows from a range based on specified columns
*
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteDuplicatesRequest
*/
async deleteDuplicates(range, comparisonColumns) {
await this._makeSingleUpdateRequest("deleteDuplicates", {
range: this._addSheetIdToRange(range),
...comparisonColumns && { comparisonColumns }
});
}
async addSlicer() {
throw new Error("Not implemented yet");
}
async updateSlicerSpec() {
throw new Error("Not implemented yet");
}
/**
* 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) {
return await this._spreadsheet.sheetsApi.post(`sheets/${this.sheetId}:copyTo`, { json: { destinationSpreadsheetId } }).json();
}
/**
* clear data in the sheet - either the entire sheet or a specific range
*/
async clear(a1Range) {
const range = a1Range ? `!${a1Range}` : "";
await this._spreadsheet.sheetsApi.post(`values/${this.encodedA1SheetName}${range}:clear`);
this.resetLocalCache(true);
}
async downloadAsCSV(returnStreamInsteadOfBuffer = false) {
return this._spreadsheet._downloadAs("csv", this.sheetId, returnStreamInsteadOfBuffer);
}
async downloadAsTSV(returnStreamInsteadOfBuffer = false) {
return this._spreadsheet._downloadAs("tsv", this.sheetId, returnStreamInsteadOfBuffer);
}
async downloadAsPDF(returnStreamInsteadOfBuffer = false) {
return this._spreadsheet._downloadAs("pdf", this.sheetId, returnStreamInsteadOfBuffer);
}
};
//#endregion
//#region src/lib/types/auth-types.ts
let AUTH_MODES = /* @__PURE__ */ function(AUTH_MODES) {
AUTH_MODES["GOOGLE_AUTH_CLIENT"] = "google_auth";
AUTH_MODES["RAW_ACCESS_TOKEN"] = "raw_access_token";
AUTH_MODES["API_KEY"] = "api_key";
return AUTH_MODES;
}({});
//#endregion
//#region src/lib/GoogleSpreadsheet.ts
const SHEETS_API_BASE_URL = "https://sheets.googleapis.com/v4/spreadsheets";
const DRIVE_API_BASE_URL = "https://www.googleapis.com/drive/v3/files";
const EXPORT_CONFIG = {
html: {},
zip: {},
xlsx: {},
ods: {},
csv: { singleWorksheet: true },
tsv: { singleWorksheet: true },
pdf: { singleWorksheet: true }
};
function getAuthMode(auth) {
if ("getRequestHeaders" in auth) return AUTH_MODES.GOOGLE_AUTH_CLIENT;
if ("token" in auth && auth.token) return AUTH_MODES.RAW_ACCESS_TOKEN;
if ("apiKey" in auth && auth.apiKey) return AUTH_MODES.API_KEY;
throw new Error("Invalid auth");
}
async function getRequestAuthConfig(auth) {
if ("getRequestHeaders" in auth) {
const headers = await auth.getRequestHeaders();
if ("entries" in headers) return { headers: Object.fromEntries(headers.entries()) };
if (isObject(headers)) return { headers };
throw new Error("unexpected headers returned from getRequestHeaders");
}
if ("apiKey" in auth && auth.apiKey) return { searchParams: { key: auth.apiKey } };
if ("token" in auth && auth.token) return { headers: { Authorization: `Bearer ${auth.token}` } };
throw new Error("Invalid auth");
}
/**
* Google Sheets document
*
* @description
* **This class represents an entire google spreadsheet document**
* Provides methods to interact with document metadata/settings, formatting, manage sheets, and acts as the main gateway to interacting with sheets and data that the document contains.q
*
*/
var GoogleSpreadsheet = class GoogleSpreadsheet {
spreadsheetId;
auth;
get authMode() {
return getAuthMode(this.auth);
}
_rawSheets;
_rawProperties = null;
_spreadsheetUrl = null;
_deleted = false;
/**
* Sheets API [ky](https://github.com/sindresorhus/ky?tab=readme-ov-file#kycreatedefaultoptions) instance
* authentication is automatically attached
* can be used if unsupported sheets calls need to be made
* @see https://developers.google.com/sheets/api/reference/rest
* */
sheetsApi;
/**
* Drive API [ky](https://github.com/sindresorhus/ky?tab=readme-ov-file#kycreatedefaultoptions) instance
* authentication automatically attached
* can be used if unsupported drive calls need to be made
* @topic permissions
* @see https://developers.google.com/drive/api/v3/reference
* */
driveApi;
/**
* initialize new GoogleSpreadsheet
* @category Initialization
* */
constructor(spreadsheetId, auth, options) {
const { retryConfig } = options || {};
this.spreadsheetId = spreadsheetId;
this.auth = auth;
this._rawSheets = {};
this._spreadsheetUrl = null;
this.sheetsApi = ky.create({
prefix: `${SHEETS_API_BASE_URL}/${spreadsheetId}`,
timeout: 18e4,
hooks: {
beforeRequest: [({ request }) => this._setAuthRequestHook(request)],
beforeError: [({ error }) => this._errorHook(error)]
},
retry: retryConfig
});
this.driveApi = ky.create({
prefix: `${DRIVE_API_BASE_URL}/${spreadsheetId}`,
hooks: {
beforeRequest: [({ request }) => this._setAuthRequestHook(request)],
beforeError: [({ error }) => this._errorHook(error)]
},
retry: retryConfig
});
}
/** @internal */
async _setAuthRequestHook(req) {
const authConfig = await getRequestAuthConfig(this.auth);
if (authConfig.headers) Object.entries(authConfig.headers).forEach(([key, val]) => {
req.headers.set(key, String(val));
});
if (authConfig.searchParams) {
const url = new URL(req.url);
Object.entries(authConfig.searchParams).forEach(([key, val]) => {
url.searchParams.set(key, String(val));
});
return new Request(url, req);
}
return req;
}
/** @internal */
async _errorHook(error) {
if (!(error instanceof HTTPError)) return error;
const errorData = typeof error.data === "string" ? (() => {
try {
return JSON.parse(error.data);
} catch {
return;
}
})() : error.data;
if (errorData?.error) {
const { code, message } = errorData.error;
error.message = `Google API error - [${code}] ${message}`;
return error;
}
if (error.response?.status === 403) {
if ("apiKey" in this.auth) throw new Error("Sheet is private. Use authentication or make public. (see https://github.com/theoephraim/node-google-spreadsheet#a-note-on-authentication for details)");
}
return error;
}
/** @internal */
async _makeSingleUpdateRequest(requestType, requestParams) {
const data = await (await this.sheetsApi.post(":batchUpdate", { json: {
requests: [{ [requestType]: requestParams }],
includeSpreadsheetInResponse: true
} })).json();
this._updateRawProperties(data.updatedSpreadsheet.properties);
each(data.updatedSpreadsheet.sheets, (s) => this._updateOrCreateSheet(s));
return data.replies[0][requestType];
}
/** @internal */
async _makeBatchUpdateRequest(requests, responseRanges) {
const data = await (await this.sheetsApi.post(":batchUpdate", { json: {
requests,
includeSpreadsheetInResponse: true,
...responseRanges && {
responseIncludeGridData: true,
...responseRanges !== "*" && { responseRanges }
}
} })).json();
this._updateRawProperties(data.updatedSpreadsheet.properties);
each(data.updatedSpreadsheet.sheets, (s) => this._updateOrCreateSheet(s));
}
/** @internal */
_ensureInfoLoaded() {
if (!this._rawProperties) throw new Error("You must call `doc.loadInfo()` before accessing this property");
}
/** @internal */
_updateRawProperties(newProperties) {
this._rawProperties = newProperties;
}
/** @internal */
_updateOrCreateSheet(sheetInfo) {
const { properties, data, protectedRanges } = sheetInfo;
const { sheetId } = properties;
if (!this._rawSheets[sheetId]) this._rawSheets[sheetId] = new GoogleSpreadsheetWorksheet(this, properties, data, protectedRanges);
else this._rawSheets[sheetId].updateRawData(properties, data, protectedRanges);
}
_getProp(param) {
this._ensureInfoLoaded();
return this._rawProperties[param];
}
get title() {
return this._getProp("title");
}
get locale() {
return this._getProp("locale");
}
get timeZone() {
return this._getProp("timeZone");
}
get autoRecalc() {
return this._getProp("autoRecalc");
}
get defaultFormat() {
return this._getProp("defaultFormat");
}
get spreadsheetTheme() {
return this._getProp("spreadsheetTheme");
}
get iterativeCalculationSettings() {
return this._getProp("iterativeCalculationSettings");
}
/**
* update spreadsheet properties
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#SpreadsheetProperties
* */
async updateProperties(properties) {
await this._makeSingleUpdateRequest("updateSpreadsheetProperties", {
properties,
fields: getFieldMask(properties)
});
}
async loadInfo(includeCells = false) {
const data = await (await this.sheetsApi.get("", { searchParams: { ...includeCells && { includeGridData: true } } })).json();
this._spreadsheetUrl = data.spreadsheetUrl;
this._rawProperties = data.properties;
data.sheets?.forEach((s) => this._updateOrCreateSheet(s));
}
resetLocalCache() {
this._rawProperties = null;
this._rawSheets = {};
}
get sheetCount() {
this._ensureInfoLoaded();
return values(this._rawSheets).length;
}
get sheetsById() {
this._ensureInfoLoaded();
return this._rawSheets;
}
get sheetsByIndex() {
this._ensureInfoLoaded();
return sortBy(this._rawSheets, "index");
}
get sheetsByTitle() {
this._ensureInfoLoaded();
return keyBy(this._rawSheets, "title");
}
/**
* Add new worksheet to document
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddSheetRequest
* */
async addSheet(properties = {}) {
const newSheetId = (await this._makeSingleUpdateRequest("addSheet", { properties: omit(properties, "headerValues", "headerRowIndex") })).properties.sheetId;
const newSheet = this.sheetsById[newSheetId];
if (properties.headerValues) await newSheet.setHeaderRow(properties.headerValues, properties.headerRowIndex);
return newSheet;
}
/**
* delete a worksheet
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteSheetRequest
* */
async deleteSheet(sheetId) {
await this._makeSingleUpdateRequest("deleteSheet", { sheetId });
delete this._rawSheets[sheetId];
}
/**
* create a new named range
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddNamedRangeRequest
*/
async addNamedRange(name, range, namedRangeId) {
return this._makeSingleUpdateRequest("addNamedRange", { namedRange: {
name,
namedRangeId,
range
} });
}
/**
* delete a named range
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteNamedRangeRequest
* */
async deleteNamedRange(namedRangeId) {
return this._makeSingleUpdateRequest("deleteNamedRange", { namedRangeId });
}
/** fetch cell data into local cache */
async loadCells(filters) {
const readOnlyMode = this.authMode === AUTH_MODES.API_KEY;
const filtersArray = isArray(filters) ? filters : [filters];
const dataFilters = map(filtersArray, (filter) => {
if (isString(filter)) return readOnlyMode ? filter : { a1Range: filter };
if (isObject(filter)) {
if (readOnlyMode) throw new Error("Only A1 ranges are supported when fetching cells with read-only access (using only an API key)");
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");
});
let result;
if (this.authMode === AUTH_MODES.API_KEY) {
const params = new URLSearchParams();
params.append("includeGridData", "true");
dataFilters.forEach((singleFilter) => {
if (!isString(singleFilter)) throw new Error("Only A1 ranges are supported when fetching cells with read-only access (using only an API key)");
params.append("ranges", singleFilter);
});
result = await this.sheetsApi.get("", { searchParams: params });
} else result = await this.sheetsApi.post(":getByDataFilter", { json: {
includeGridData: true,
dataFilters
} });
const data = await result?.json();
each(data.sheets, (sheet) => {
this._updateOrCreateSheet(sheet);
});
}
/**
* export/download helper, not meant to be called directly (use downloadAsX methods on spreadsheet and worksheet instead)
* @internal
*/
async _downloadAs(fileType, worksheetId, returnStreamInsteadOfBuffer) {
if (!EXPORT_CONFIG[fileType]) throw new Error(`unsupported export fileType - ${fileType}`);
if (EXPORT_CONFIG[fileType].singleWorksheet) {
if (worksheetId === void 0) throw new Error(`Must specify worksheetId when exporting as ${fileType}`);
} else if (worksheetId) throw new Error(`Cannot specify worksheetId when exporting as ${fileType}`);
if (fileType === "html") fileType = "zip";
if (!this._spreadsheetUrl) throw new Error("Cannot export sheet that is not fully loaded");
const exportUrl = this._spreadsheetUrl.replace("edit", "export");
const response = await this.sheetsApi.get(exportUrl, {
prefix: "",
searchParams: {
id: this.spreadsheetId,
format: fileType,
...worksheetId !== void 0 && { gid: worksheetId }
}
});
if (returnStreamInsteadOfBuffer) return response.body;
return response.arrayBuffer();
}
async downloadAsZippedHTML(returnStreamInsteadOfBuffer) {
return this._downloadAs("html", void 0, returnStreamInsteadOfBuffer);
}
/**
* @deprecated
* use `doc.downloadAsZippedHTML()` instead
* */
async downloadAsHTML(returnStreamInsteadOfBuffer) {
return this._downloadAs("html", void 0, returnStreamInsteadOfBuffer);
}
async downloadAsXLSX(returnStreamInsteadOfBuffer = false) {
return this._downloadAs("xlsx", void 0, returnStreamInsteadOfBuffer);
}
async downloadAsODS(returnStreamInsteadOfBuffer = false) {
return this._downloadAs("ods", void 0, returnStreamInsteadOfBuffer);
}
async delete() {
await this.driveApi.delete("");
this._deleted = true;
}
/**
* list all permissions entries for doc
*/
async listPermissions() {
return (await (await this.driveApi.get("permissions", { searchParams: { fields: "permissions(id,type,emailAddress,domain,role,displayName,photoLink,deleted)" } })).json()).permissions;
}
async setPublicAccessLevel(role) {
const permissions = await this.listPermissions();
const existingPublicPermission = find(permissions, (p) => p.type === "anyone");
if (role === false) {
if (!existingPublicPermission) return;
await this.driveApi.delete(`permissions/${existingPublicPermission.id}`);
} else await this.driveApi.post("permissions", { json: {
role: role || "viewer",
type: "anyone"
} });
}
/** share document to email or domain */
async share(emailAddressOrDomain, opts) {
let emailAddress;
let domain;
if (emailAddressOrDomain.includes("@")) emailAddress = emailAddressOrDomain;
else domain = emailAddressOrDomain;
return (await this.driveApi.post("permissions", {
searchParams: {
...opts?.emailMessage === false && { sendNotificationEmail: false },
...isString(opts?.emailMessage) && { emailMessage: opts?.emailMessage },
...opts?.role === "owner" && { transferOwnership: true }
},
json: {
role: opts?.role || "writer",
...emailAddress && {
type: opts?.isGroup ? "group" : "user",
emailAddress
},
...domain && {
type: "domain",
domain
}
}
})).json();
}
/**
* delete a permission by its ID
* @see https://developers.google.com/drive/api/v3/reference/permissions/delete
*/
async deletePermission(permissionId) {
await this.driveApi.delete(`permissions/${permissionId}`);
}
/**
* search for developer metadata entries matching the given filters
* @see https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.developerMetadata/search
*/
async searchDeveloperMetadata(filters) {
const data = await (await this.sheetsApi.post("developerMetadata:search", { json: { dataFilters: filters } })).json();
if (!data.matchedDeveloperMetadata) return [];
return data.matchedDeveloperMetadata.map((m) => m.developerMetadata);
}
static async createNewSpreadsheetDocument(auth, properties) {
if (getAuthMode(auth) === AUTH_MODES.API_KEY) throw new Error("Cannot use api key only to create a new spreadsheet - it is only usable for read-only access of public docs");
const authConfig = await getRequestAuthConfig(auth);
const data = await (await ky.post(SHEETS_API_BASE_URL, {
...authConfig,
json: { properties }
})).json();
const newSpreadsheet = new GoogleSpreadsheet(data.spreadsheetId, auth);
newSpreadsheet._spreadsheetUrl = data.spreadsheetUrl;
newSpreadsheet._rawProperties = data.properties;
each(data.sheets, (s) => newSpreadsheet._updateOrCreateSheet(s));
return newSpreadsheet;
}
};
//#endregion
export { GoogleSpreadsheet, GoogleSpreadsheetCell, GoogleSpreadsheetCellErrorValue, GoogleSpreadsheetRow, GoogleSpreadsheetWorksheet };
//# sourceMappingURL=index.js.map