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 +85 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/providers/google.d.ts +2 -0
- package/dist/src/providers/google.js +2 -0
- package/dist/src/providers/googleProvider.d.ts +26 -0
- package/dist/src/providers/googleProvider.js +168 -0
- package/dist/src/providers/sheetClient.d.ts +21 -0
- package/dist/src/providers/sheetClient.js +29 -0
- package/dist/src/providers/types.d.ts +39 -0
- package/dist/src/providers/types.js +1 -0
- package/dist/src/utils/inputParser.d.ts +1 -0
- package/dist/src/utils/inputParser.js +9 -0
- package/dist/test/providers/googleProvider.integration.test.d.ts +1 -0
- package/dist/test/providers/googleProvider.integration.test.js +45 -0
- package/dist/test/providers/googleProvider.test.d.ts +1 -0
- package/dist/test/providers/googleProvider.test.js +63 -0
- package/package.json +51 -0
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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|