UNPKG

google-spreadsheet

Version:

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

1,925 lines 66.2 kB
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