Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .bumpy/update-deps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
google-spreadsheet: minor
---

upgrade ky from v1 to v2

If you use `doc.sheetsApi` or `doc.driveApi` directly, note that ky v2 changed its hook signatures (hooks now receive state objects instead of direct Request/Error params) and renamed `prefixUrl` to `prefix`. See the [ky v2 migration guide](https://github.com/sindresorhus/ky/releases/tag/v2.0.0) for details.
2 changes: 1 addition & 1 deletion .env.schema
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ CI=

# Delay in ms to use when running tests
# @type=number
TEST_DELAY=200
TEST_DELAY=if($CI, 300, 200)
16 changes: 8 additions & 8 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@
"test:ci": "vitest run"
},
"dependencies": {
"es-toolkit": "^1.44.0",
"ky": "^1.14.3"
"es-toolkit": "^1.46.1",
"ky": "^2.0.2"
},
"devDependencies": {
"@types/node": "^25.2.3",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"@varlock/bumpy": "^1.5.1",
"@varlock/bumpy": "^1.8.1",
"docsify-cli": "^4.4.4",
"eslint": "^8.41.0",
"eslint-config-airbnb-base": "^15.0.0",
Expand All @@ -69,7 +69,7 @@
"nock": "^14.0.11",
"tsdown": "^0.20.3",
"typescript": "^5.9.3",
"varlock": "^1.0.0",
"varlock": "^1.1.0",
"vitest": "^4.0.18"
},
"peerDependencies": {
Expand Down
39 changes: 19 additions & 20 deletions src/lib/GoogleSpreadsheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,19 +142,23 @@ export class GoogleSpreadsheet {

// create a ky instance with sheet root URL and hooks to handle auth
this.sheetsApi = ky.create({
prefixUrl: `${SHEETS_API_BASE_URL}/${spreadsheetId}`,
prefix: `${SHEETS_API_BASE_URL}/${spreadsheetId}`,
timeout: 180_000,
hooks: {
beforeRequest: [(r) => this._setAuthRequestHook(r)],
beforeError: [(e) => this._errorHook(e)],
beforeRequest: [
({ request }) => this._setAuthRequestHook(request),
],
beforeError: [
({ error }) => this._errorHook(error),
],
},
retry: retryConfig,
});
this.driveApi = ky.create({
prefixUrl: `${DRIVE_API_BASE_URL}/${spreadsheetId}`,
prefix: `${DRIVE_API_BASE_URL}/${spreadsheetId}`,
hooks: {
beforeRequest: [(r) => this._setAuthRequestHook(r)],
beforeError: [(e) => this._errorHook(e)],
beforeRequest: [({ request }) => this._setAuthRequestHook(request)],
beforeError: [({ error }) => this._errorHook(error)],
},
retry: retryConfig,
});
Expand Down Expand Up @@ -185,26 +189,21 @@ export class GoogleSpreadsheet {
}

/** @internal */
async _errorHook(error: HTTPError) {
const { response } = error;
const errorDataText = await response?.text();
let errorData;
try {
errorData = JSON.parse(errorDataText);
} catch (e) {
// console.log('parsing json failed', errorDataText);
}
async _errorHook(error: Error) {
if (!(error instanceof HTTPError)) return error;

if (errorData) {
// usually the error has a code and message, but occasionally not
if (!errorData.error) return error;
// ky pre-parses the response body into error.data (the response body is already consumed)
const errorData = typeof error.data === 'string' ? (() => {
try { return JSON.parse(error.data as string); } catch { return undefined; }
})() : error.data;

if (errorData?.error) {
const { code, message } = errorData.error;
error.message = `Google API error - [${code}] ${message}`;
return error;
}

if (_.get(error, 'response.status') === 403) {
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)');
}
Expand Down Expand Up @@ -494,7 +493,7 @@ export class GoogleSpreadsheet {

const exportUrl = this._spreadsheetUrl.replace('edit', 'export');
const response = await this.sheetsApi.get(exportUrl, {
prefixUrl: '', // unset baseUrl since we're not hitting the normal sheets API
prefix: '', // unset baseUrl since we're not hitting the normal sheets API
searchParams: {
id: this.spreadsheetId,
format: fileType,
Expand Down
70 changes: 70 additions & 0 deletions src/test/exports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
describe, expect, it, beforeAll, afterAll, afterEach,
} from 'vitest';
import { setTimeout as delay } from 'timers/promises';

import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from '..';

import { DOC_IDS, testServiceAccountAuth } from './auth/docs-and-auth';

const doc = new GoogleSpreadsheet(DOC_IDS.private, testServiceAccountAuth);
let sheet: GoogleSpreadsheetWorksheet;

describe('Export/download methods', () => {
beforeAll(async () => {
await doc.loadInfo();
sheet = await doc.addSheet({
title: `Export test ${+new Date()}`,
headerValues: ['name', 'value'],
});
await sheet.addRows([
{ name: 'Alice', value: '100' },
{ name: 'Bob', value: '200' },
{ name: 'Charlie', value: '300' },
]);
});

afterAll(async () => {
await sheet.delete();
});

// export endpoint has tight rate limits
afterEach(async () => delay(3000));

it('can download document as XLSX', async () => {
const buffer = await doc.downloadAsXLSX();
expect(buffer).toBeInstanceOf(ArrayBuffer);
expect(buffer.byteLength).toBeGreaterThan(0);
});

it('can download worksheet as CSV and verify content', async () => {
const buffer = await sheet.downloadAsCSV();
expect(buffer).toBeInstanceOf(ArrayBuffer);
expect(buffer.byteLength).toBeGreaterThan(0);

const csvText = new TextDecoder().decode(buffer);
const lines = csvText.trim().split('\n');

expect(lines[0]).toContain('name');
expect(lines[0]).toContain('value');
expect(lines[1]).toContain('Alice');
expect(lines[2]).toContain('Bob');
expect(lines[3]).toContain('Charlie');
});

it('can download worksheet as TSV', async () => {
const buffer = await sheet.downloadAsTSV();
expect(buffer).toBeInstanceOf(ArrayBuffer);
expect(buffer.byteLength).toBeGreaterThan(0);

const tsvText = new TextDecoder().decode(buffer);
expect(tsvText).toContain('\t');
expect(tsvText).toContain('Alice');
});

it('can download worksheet as PDF', async () => {
const buffer = await sheet.downloadAsPDF();
expect(buffer).toBeInstanceOf(ArrayBuffer);
expect(buffer.byteLength).toBeGreaterThan(0);
});
});
Loading