google-sheets-automation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+
2
+ # google-sheets-automation
3
+
4
+ Simplify populating sheets from inputs. Designed to be extensible for multiple sheet providers, but currently only supports Google Sheets. Usable in Node.js projects (backend or CLI).
5
+
6
+ # google-sheets-automation
7
+ - Add rows, update cells, batch updates
8
+ Automate populating and managing Google Sheets from Node.js. Usable in backend, CLI, or serverless environments.
9
+ - Accepts objects, arrays, or form data
10
+ - Simple, promise-based API
11
+ - Add rows, update cells, batch updates
12
+ - Google Sheets API support
13
+ - Accepts objects, arrays, or form data
14
+ - Simple, promise-based API
15
+
16
+ ```js
17
+ ## Usage Example
18
+ npm install google-sheets-automation
19
+ import { GoogleSheetProvider } from 'google-sheets-automation';
20
+
21
+ const provider = new GoogleSheetProvider({
22
+ serviceAccount: {
23
+ import { GoogleSheetProvider } from 'google-sheets-automation';
24
+ private_key: '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n',
25
+ },
26
+ sheetId: 'your-google-sheet-id',
27
+ });
28
+
29
+ const headerMap = {
30
+ Name: 'name',
31
+ Email: 'email',
32
+ Message: 'message',
33
+ };
34
+
35
+ const rows = [
36
+ { name: 'Alice', email: 'alice@example.com', message: 'Hello!' },
37
+ { name: 'Bob', email: 'bob@example.com', message: 'Hi!' },
38
+ ];
39
+
40
+ await provider.addRows(rows, headerMap);
41
+ ```
42
+
43
+ ## Testing
44
+ Unit tests are written with Jest and mock all Google API calls, so no credentials are required:
45
+ ```bash
46
+ npm run test
47
+ ```
48
+
49
+ ### Integration Testing
50
+ To run integration tests against the real Google Sheets API, you must:
51
+ - Set up a `.env` file with valid `GOOGLE_SERVICE_ACCOUNT_EMAIL`, `GOOGLE_PRIVATE_KEY`, and `GOOGLE_SHEET_ID`.
52
+ - Ensure the target sheet (e.g. `IntegrationTestSheet`) exists in your Google Spreadsheet before running the test.
53
+ - Run:
54
+ ```bash
55
+ npm run test test/providers/googleProvider.integration.test.ts
56
+ ```
57
+ Integration tests will use your credentials and make real API calls. Do not run these against production data.
58
+
59
+
60
+ ## Providers
61
+ - `GoogleSheetProvider` (currently available)
62
+ - Usage: `new GoogleSheetProvider({ ...credentials })`
63
+ -(Planned) ExcelSheetProvider, AirtableSheetProvider, etc.
64
+
65
+ ## API Functions (ISheetProvider)
66
+ All providers implement the following methods:
67
+
68
+ `addRows(options, rows)` — Add rows to the sheet, optionally mapping headers.
69
+ `updateCells(options)` — Update cells in the sheet (A1 range, 2D values).
70
+ `readData(options)` — Read data from the sheet (returns array of objects).
71
+ `deleteRows(options)` — Delete rows by index (not yet implemented).
72
+ `createSheet(options)` — Create a new sheet (not yet implemented).
73
+
74
+ ### Types
75
+ `AddRowsOptions`: `{ spreadsheetId, sheetName, headerMap? }`
76
+ `UpdateCellsOptions`: `{ spreadsheetId, sheetName, range?, values }`
77
+ `ReadDataOptions`: `{ spreadsheetId, sheetName, range? }`
78
+ `DeleteRowsOptions`: `{ spreadsheetId, sheetName, rowIndexes }`
79
+ `CreateSheetOptions`: `{ spreadsheetId, sheetName }`
80
+
81
+ ## Roadmap
82
+ - Support for additional sheet providers (Excel, Airtable, etc.) planned for future releases.
83
+
84
+ ## License
85
+ MIT
@@ -0,0 +1 @@
1
+ export * from './providers/google';
@@ -0,0 +1 @@
1
+ export * from './providers/google';
@@ -0,0 +1,2 @@
1
+ export { GoogleSheetProvider, ISheetProvider } from './googleProvider';
2
+ export { SheetClient, ISheetClient } from './sheetClient';
@@ -0,0 +1,2 @@
1
+ export { GoogleSheetProvider } from './googleProvider';
2
+ export { SheetClient } from './sheetClient';
@@ -0,0 +1,26 @@
1
+ import { SheetRow, AddRowsOptions, UpdateCellsOptions, ReadDataOptions, DeleteRowsOptions, CreateSheetOptions } from './types';
2
+ export interface ISheetProvider {
3
+ addRows(options: AddRowsOptions, rows: SheetRow[]): Promise<void>;
4
+ updateCells(options: UpdateCellsOptions): Promise<void>;
5
+ readData(options: ReadDataOptions): Promise<SheetRow[]>;
6
+ deleteRows(options: DeleteRowsOptions): Promise<void>;
7
+ createSheet(options: CreateSheetOptions): Promise<void>;
8
+ }
9
+ export declare class GoogleSheetProvider implements ISheetProvider {
10
+ private getBaseUrl;
11
+ private buildUrl;
12
+ private readonly credentials;
13
+ private _accessToken;
14
+ private _tokenExpiry;
15
+ constructor(credentials: any);
16
+ /**
17
+ * Get a valid access token, either from credentials or by generating from service account.
18
+ */
19
+ private getAccessToken;
20
+ addRows(options: AddRowsOptions, rows: SheetRow[]): Promise<void>;
21
+ updateCells(options: UpdateCellsOptions): Promise<void>;
22
+ readData(options: ReadDataOptions): Promise<SheetRow[]>;
23
+ deleteRows(options: DeleteRowsOptions): Promise<void>;
24
+ createSheet(options: CreateSheetOptions): Promise<void>;
25
+ static mapHeaders(input: SheetRow, headerMap?: Record<string, string>): SheetRow;
26
+ }
@@ -0,0 +1,168 @@
1
+ // Only import google-auth-library in Node.js
2
+ let GoogleAuth = undefined;
3
+ if (typeof process !== 'undefined' && process.versions?.node) {
4
+ try {
5
+ GoogleAuth = require('google-auth-library').GoogleAuth;
6
+ }
7
+ catch { }
8
+ }
9
+ export class GoogleSheetProvider {
10
+ getBaseUrl() {
11
+ return 'https://sheets.googleapis.com/v4/spreadsheets';
12
+ }
13
+ buildUrl(path, params) {
14
+ let url = `${this.getBaseUrl()}/${path}`;
15
+ if (params) {
16
+ const query = Object.entries(params)
17
+ .filter(([_, v]) => v !== undefined)
18
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
19
+ .join('&');
20
+ if (query)
21
+ url += `?${query}`;
22
+ }
23
+ return url;
24
+ }
25
+ constructor(credentials) {
26
+ this._accessToken = null;
27
+ this._tokenExpiry = null;
28
+ this.credentials = credentials;
29
+ }
30
+ /**
31
+ * Get a valid access token, either from credentials or by generating from service account.
32
+ */
33
+ async getAccessToken() {
34
+ if (this.credentials.accessToken) {
35
+ return this.credentials.accessToken;
36
+ }
37
+ // If service account credentials are provided (Node.js only)
38
+ if (GoogleAuth && this.credentials.private_key && this.credentials.client_email) {
39
+ if (this._accessToken && this._tokenExpiry && Date.now() < this._tokenExpiry - 60000) {
40
+ return this._accessToken;
41
+ }
42
+ const auth = new GoogleAuth({
43
+ credentials: this.credentials,
44
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
45
+ });
46
+ const client = await auth.getClient();
47
+ const { token, res } = await client.getAccessToken();
48
+ let expiry = Date.now() + 50 * 60 * 1000;
49
+ if (res?.data?.expiry_date) {
50
+ expiry = res.data.expiry_date;
51
+ }
52
+ this._accessToken = token;
53
+ this._tokenExpiry = expiry;
54
+ return token;
55
+ }
56
+ throw new Error('No valid access token or service account credentials provided.');
57
+ }
58
+ async addRows(options, rows) {
59
+ const { spreadsheetId, sheetName, headerMap } = options;
60
+ const accessToken = await this.getAccessToken();
61
+ let values = [];
62
+ // If headerMap is passed and sheet is empty, add header row first
63
+ if (headerMap) {
64
+ // Check if sheet is empty by reading first row
65
+ const readUrl = this.buildUrl(`${spreadsheetId}/values/${encodeURIComponent(sheetName)}`, { majorDimension: 'ROWS', range: sheetName });
66
+ const readRes = await fetch(readUrl, {
67
+ method: 'GET',
68
+ headers: {
69
+ 'Authorization': `Bearer ${accessToken}`,
70
+ },
71
+ });
72
+ let sheetIsEmpty = true;
73
+ if (readRes.ok) {
74
+ const data = await readRes.json();
75
+ if (data.values && data.values.length > 0) {
76
+ sheetIsEmpty = false;
77
+ }
78
+ }
79
+ if (sheetIsEmpty) {
80
+ // Add header row
81
+ values.push(Object.values(headerMap));
82
+ }
83
+ }
84
+ // Map headers if needed
85
+ const mappedRows = headerMap
86
+ ? rows.map(row => GoogleSheetProvider.mapHeaders(row, headerMap))
87
+ : rows;
88
+ values = values.concat(mappedRows.map(row => Object.values(row)));
89
+ // Prepare API request
90
+ const url = this.buildUrl(`${spreadsheetId}/values/${encodeURIComponent(sheetName)}:append`, { valueInputOption: 'USER_ENTERED' });
91
+ const body = JSON.stringify({ values });
92
+ const res = await fetch(url, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Authorization': `Bearer ${accessToken}`,
96
+ 'Content-Type': 'application/json'
97
+ },
98
+ body
99
+ });
100
+ if (!res.ok) {
101
+ const err = await res.text();
102
+ throw new Error(`Google Sheets API error: ${res.status} ${err}`);
103
+ }
104
+ }
105
+ async updateCells(options) {
106
+ const { spreadsheetId, sheetName, range, values } = options;
107
+ const accessToken = await this.getAccessToken();
108
+ // values should be a 2D array matching the range
109
+ const url = this.buildUrl(`${spreadsheetId}/values/${encodeURIComponent(range || sheetName)}`, { valueInputOption: 'USER_ENTERED' });
110
+ const body = JSON.stringify({ values });
111
+ const res = await fetch(url, {
112
+ method: 'PUT',
113
+ headers: {
114
+ 'Authorization': `Bearer ${accessToken}`,
115
+ 'Content-Type': 'application/json'
116
+ },
117
+ body
118
+ });
119
+ if (!res.ok) {
120
+ const err = await res.text();
121
+ throw new Error(`Google Sheets API error: ${res.status} ${err}`);
122
+ }
123
+ }
124
+ async readData(options) {
125
+ const { spreadsheetId, sheetName, range } = options;
126
+ const accessToken = await this.getAccessToken();
127
+ const url = this.buildUrl(`${spreadsheetId}/values/${encodeURIComponent(range || sheetName)}`, { majorDimension: 'ROWS' });
128
+ const res = await fetch(url, {
129
+ method: 'GET',
130
+ headers: {
131
+ 'Authorization': `Bearer ${accessToken}`,
132
+ },
133
+ });
134
+ if (!res.ok) {
135
+ const err = await res.text();
136
+ throw new Error(`Google Sheets API error: ${res.status} ${err}`);
137
+ }
138
+ const data = await res.json();
139
+ if (!data.values || data.values.length === 0)
140
+ return [];
141
+ // Assume first row is header
142
+ const headers = data.values[0];
143
+ const rows = [];
144
+ for (let i = 1; i < data.values.length; i++) {
145
+ const row = {};
146
+ for (let j = 0; j < headers.length; j++) {
147
+ row[headers[j]] = data.values[i][j];
148
+ }
149
+ rows.push(row);
150
+ }
151
+ return rows;
152
+ }
153
+ async deleteRows(options) {
154
+ // TODO: Implement row deletion logic
155
+ }
156
+ async createSheet(options) {
157
+ // TODO: Implement sheet creation logic
158
+ }
159
+ static mapHeaders(input, headerMap) {
160
+ if (!headerMap)
161
+ return input;
162
+ const mapped = {};
163
+ for (const key in input) {
164
+ mapped[headerMap[key] || key] = input[key];
165
+ }
166
+ return mapped;
167
+ }
168
+ }
@@ -0,0 +1,21 @@
1
+ import { SheetRow, AddRowsOptions, UpdateCellsOptions, ReadDataOptions, DeleteRowsOptions, CreateSheetOptions } from './types';
2
+ export interface ISheetClient {
3
+ addRows(options: AddRowsOptions, rows: SheetRow[]): Promise<void>;
4
+ updateCells(options: UpdateCellsOptions): Promise<void>;
5
+ readData(options: ReadDataOptions): Promise<SheetRow[]>;
6
+ deleteRows(options: DeleteRowsOptions): Promise<void>;
7
+ createSheet(options: CreateSheetOptions): Promise<void>;
8
+ }
9
+ export declare class SheetClient implements ISheetClient {
10
+ private readonly provider;
11
+ constructor(config: {
12
+ provider: string;
13
+ credentials: any;
14
+ });
15
+ addRows(options: AddRowsOptions, rows: SheetRow[]): Promise<void>;
16
+ updateCells(options: UpdateCellsOptions): Promise<void>;
17
+ readData(options: ReadDataOptions): Promise<SheetRow[]>;
18
+ deleteRows(options: DeleteRowsOptions): Promise<void>;
19
+ createSheet(options: CreateSheetOptions): Promise<void>;
20
+ static mapHeaders(input: SheetRow, headerMap?: Record<string, string>): SheetRow;
21
+ }
@@ -0,0 +1,29 @@
1
+ import { GoogleSheetProvider } from './googleProvider';
2
+ export class SheetClient {
3
+ constructor(config) {
4
+ if (config.provider === 'google') {
5
+ this.provider = new GoogleSheetProvider(config.credentials);
6
+ }
7
+ else {
8
+ throw new Error('Provider not supported');
9
+ }
10
+ }
11
+ addRows(options, rows) {
12
+ return this.provider.addRows(options, rows);
13
+ }
14
+ updateCells(options) {
15
+ return this.provider.updateCells(options);
16
+ }
17
+ readData(options) {
18
+ return this.provider.readData(options);
19
+ }
20
+ deleteRows(options) {
21
+ return this.provider.deleteRows(options);
22
+ }
23
+ createSheet(options) {
24
+ return this.provider.createSheet(options);
25
+ }
26
+ static mapHeaders(input, headerMap) {
27
+ return GoogleSheetProvider.mapHeaders(input, headerMap);
28
+ }
29
+ }
@@ -0,0 +1,39 @@
1
+ export interface SheetRow {
2
+ [key: string]: string | number | boolean | null;
3
+ }
4
+ export interface SheetRange {
5
+ startRow: number;
6
+ endRow: number;
7
+ startCol: number;
8
+ endCol: number;
9
+ }
10
+ export interface SheetOptions {
11
+ spreadsheetId: string;
12
+ sheetName: string;
13
+ }
14
+ export interface AddRowsOptions extends SheetOptions {
15
+ headerMap?: Record<string, string>;
16
+ }
17
+ export interface UpdateCellsOptions extends SheetOptions {
18
+ /**
19
+ * A1 notation range to update, e.g. "Sheet1!A2:B3". If omitted, uses sheetName.
20
+ */
21
+ range?: string;
22
+ /**
23
+ * 2D array of values to write to the range.
24
+ */
25
+ values: (string | number | boolean | null)[][];
26
+ }
27
+ export interface ReadDataOptions extends SheetOptions {
28
+ /**
29
+ * A1 notation range to read, e.g. "Sheet1!A2:B3". If omitted, uses sheetName.
30
+ */
31
+ range?: string;
32
+ }
33
+ export interface DeleteRowsOptions extends SheetOptions {
34
+ rowIndexes: number[];
35
+ }
36
+ export interface CreateSheetOptions {
37
+ spreadsheetId: string;
38
+ sheetName: string;
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function parseInput(input: any): any[];
@@ -0,0 +1,9 @@
1
+ // Utility to parse various input formats into rows for sheets
2
+ export function parseInput(input) {
3
+ if (Array.isArray(input))
4
+ return input;
5
+ if (typeof input === 'object')
6
+ return [input];
7
+ // Add more parsing logic as needed
8
+ throw new Error('Unsupported input format');
9
+ }
@@ -0,0 +1,45 @@
1
+ import dotenv from 'dotenv';
2
+ dotenv.config();
3
+ import { GoogleSheetProvider } from '../../src/providers/googleProvider';
4
+ describe('GoogleSheetProvider (integration)', () => {
5
+ const credentials = {
6
+ private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
7
+ client_email: process.env.GOOGLE_CLIENT_EMAIL,
8
+ };
9
+ const spreadsheetId = process.env.GOOGLE_SHEET_ID;
10
+ const sheetName = 'IntegrationTestSheet';
11
+ const addOptions = {
12
+ spreadsheetId,
13
+ sheetName,
14
+ headerMap: { name: 'Name', email: 'Email' }
15
+ };
16
+ const rows = [
17
+ { name: 'Integration Alice', email: 'alice@example.com' },
18
+ { name: 'Integration Bob', email: 'bob@example.com' }
19
+ ];
20
+ it('should add header and data rows to the sheet (real API)', async () => {
21
+ const provider = new GoogleSheetProvider(credentials);
22
+ await expect(provider.addRows(addOptions, rows)).resolves.not.toThrow();
23
+ });
24
+ it('should update cells in the sheet (real API)', async () => {
25
+ const provider = new GoogleSheetProvider(credentials);
26
+ const updateOptions = {
27
+ spreadsheetId,
28
+ sheetName,
29
+ range: `${sheetName}!A2:B2`,
30
+ values: [['Integration Alice', 'alice@example.com']]
31
+ };
32
+ await expect(provider.updateCells(updateOptions)).resolves.not.toThrow();
33
+ });
34
+ it('should read data from the sheet (real API)', async () => {
35
+ const provider = new GoogleSheetProvider(credentials);
36
+ const readOptions = {
37
+ spreadsheetId,
38
+ sheetName
39
+ };
40
+ const data = await provider.readData(readOptions);
41
+ expect(data.length).toBeGreaterThan(0);
42
+ expect(data[0]).toHaveProperty('Name');
43
+ expect(data[0]).toHaveProperty('Email');
44
+ });
45
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import { GoogleSheetProvider } from '../../src/providers/googleProvider';
2
+ describe('GoogleSheetProvider', () => {
3
+ // Mock network and auth
4
+ beforeAll(() => {
5
+ jest.spyOn(GoogleSheetProvider.prototype, 'getAccessToken').mockResolvedValue('fake-token');
6
+ global.fetch = jest.fn().mockImplementation((url, opts) => {
7
+ // Simulate Google Sheets API responses
8
+ if (url.includes(':append')) {
9
+ return Promise.resolve({ ok: true, json: async () => ({}) });
10
+ }
11
+ if (opts?.method === 'PUT') {
12
+ return Promise.resolve({ ok: true, json: async () => ({}) });
13
+ }
14
+ if (opts?.method === 'GET') {
15
+ // Simulate reading data with header and two rows
16
+ return Promise.resolve({
17
+ ok: true,
18
+ json: async () => ({ values: [['Name', 'Email'], ['Alice', 'alice@example.com'], ['Bob', 'bob@example.com']] })
19
+ });
20
+ }
21
+ return Promise.resolve({ ok: true, json: async () => ({}) });
22
+ });
23
+ });
24
+ afterAll(() => {
25
+ jest.restoreAllMocks();
26
+ });
27
+ const credentials = { client_email: 'test@example.com', private_key: 'fake-key' };
28
+ const addOptions = {
29
+ spreadsheetId: 'fake-sheet-id',
30
+ sheetName: 'TestSheet',
31
+ headerMap: { name: 'Name', email: 'Email' }
32
+ };
33
+ const rows = [
34
+ { name: 'Alice', email: 'alice@example.com' },
35
+ { name: 'Bob', email: 'bob@example.com' }
36
+ ];
37
+ it('should add header and data rows to the sheet', async () => {
38
+ const provider = new GoogleSheetProvider(credentials);
39
+ await expect(provider.addRows(addOptions, rows)).resolves.not.toThrow();
40
+ });
41
+ it('should update cells in the sheet', async () => {
42
+ const provider = new GoogleSheetProvider(credentials);
43
+ const updateOptions = {
44
+ spreadsheetId: 'fake-sheet-id',
45
+ sheetName: 'TestSheet',
46
+ range: 'TestSheet!A2:B2',
47
+ values: [['Alice', 'alice@example.com']]
48
+ };
49
+ await expect(provider.updateCells(updateOptions)).resolves.not.toThrow();
50
+ });
51
+ it('should read data from the sheet', async () => {
52
+ const provider = new GoogleSheetProvider(credentials);
53
+ const readOptions = {
54
+ spreadsheetId: 'fake-sheet-id',
55
+ sheetName: 'TestSheet'
56
+ };
57
+ const data = await provider.readData(readOptions);
58
+ expect(data).toEqual([
59
+ { Name: 'Alice', Email: 'alice@example.com' },
60
+ { Name: 'Bob', Email: 'bob@example.com' }
61
+ ]);
62
+ });
63
+ });
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "google-sheets-automation",
3
+ "version": "0.1.0",
4
+ "description": "Automate Google Sheets from Node.js: add, update, and read rows with simple API. Supports service accounts, header mapping, and batch operations.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "node --experimental-vm-modules node_modules/.bin/jest"
20
+ },
21
+ "keywords": [
22
+ "google-sheets",
23
+ "spreadsheet",
24
+ "api",
25
+ "automation",
26
+ "nodejs",
27
+ "provider"
28
+ ],
29
+ "author": "CaesarSage",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/CaesarSage/google-sheets-automation.git"
34
+ },
35
+ "devDependencies": {
36
+ "@types/jest": "^30.0.0",
37
+ "@types/node": "^25.0.9",
38
+ "@types/react": "^19.2.9",
39
+ "@types/react-dom": "^19.2.3",
40
+ "dotenv": "^17.2.3",
41
+ "jest": "^30.2.0",
42
+ "ts-jest": "^29.4.6",
43
+ "typescript": "^5.0.0"
44
+ },
45
+ "dependencies": {
46
+ "google-auth-library": "^10.5.0",
47
+ "next": "^16.1.4",
48
+ "react": "^19.2.3",
49
+ "react-dom": "^19.2.3"
50
+ }
51
+ }