├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── GoogleSheet.ts ├── RowMapper.ts └── interfaces │ ├── IGoogleSheetOptions.ts │ └── ValueInputOption.ts ├── tests └── GoogleSheetTest.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | *.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 6 5 | - 5 6 | install: 7 | - npm install --dev 8 | script: 9 | - npm test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Xavier Decuyper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # google-sheets-node 2 | 3 | [![Current version](https://img.shields.io/npm/v/google-sheets-wrapper.svg)](https://www.npmjs.com/package/google-sheets-wrapper) 4 | [![Downloads on npm](https://img.shields.io/npm/dt/google-sheets-wrapper.svg)](https://www.npmjs.com/package/google-sheets-wrapper) 5 | [![License](https://img.shields.io/npm/l/google-sheets-wrapper.svg)](/LICENSE) 6 | [![Build status](https://img.shields.io/travis/Savjee/google-sheets-wrapper.svg)](https://travis-ci.org/Savjee/google-sheets-wrapper) 7 | [![Dependencies](https://img.shields.io/david/savjee/google-sheets-wrapper.svg)](https://www.npmjs.com/package/google-sheets-wrapper) 8 | 9 | A lightweight wrapper around the official Google Sheets API that makes it easy to read and write rows. It's written in TypeScript and uses async/await to handle requests to Google's API. 10 | 11 | # Usage 12 | The library only supports interacting with rows in Google Sheets. Not with columns or individual cells. 13 | 14 | When fetching rows, the library will map your data to Javascript objects. When inserting new rows, you can also use objects. 15 | 16 | ## Authentication 17 | Follow step 1 of the official "Node.js Quickstart": [https://developers.google.com/sheets/api/quickstart/nodejs](https://developers.google.com/sheets/api/quickstart/nodejs). This will walk you through enabling the Sheets API and creating credentials (JSON file). 18 | 19 | Afterwards set the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` so that it contains the path to your credentials file. Could be something like this: 20 | 21 | ```bash 22 | export GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json 23 | ``` 24 | 25 | Other methods of authenticating are currently not supported. 26 | 27 | ## Header row 28 | This library assumes that the first row in your spreadsheet is used as a header. 29 | 30 | ![](https://savjee.github.io/google-sheets-wrapper/screenshots/header-row.png) 31 | 32 | The header row is used to transform your rows to Javascript objects. Try to keep the values in the header simple (no spaces, no special characters, ...) The library will convert your titles to camelCase, so be aware of this. For example: ``Time posted`` will be converted to ``timePosted``. 33 | 34 | ## Getting rows 35 | This will map your rows to objects, using the first row of your spreadsheet as header (see "Header row"): 36 | 37 | ```javascript 38 | // Open spreadsheet with ID XXXX-XXXX-XXXX and work with columns A to C in worksheet "Sheet 1" 39 | let sheet = new GoogleSheet({ 40 | sheetId: "XXXXXXXXXX-XXXXXXXXX-XXXXXXXX", 41 | range: "'Sheet 1'!A:C" 42 | }); 43 | 44 | // Get the data 45 | let data = await sheet.getRows(); 46 | 47 | // Show it 48 | console.log(data); 49 | ``` 50 | 51 | For example, this spreadsheet: 52 | 53 | ![](https://savjee.github.io/google-sheets-wrapper/screenshots/simple-spreadsheet.png) 54 | 55 | Will be mapped to this: 56 | ``` 57 | [ 58 | { timestamp: '1488806320466', message: 'Hi there!', user: 'Xavier' }, 59 | { timestamp: '1488806320467', message: 'We meet again.', user: 'Xavier' } 60 | ] 61 | ``` 62 | 63 | ## Writing new rows 64 | To write new rows you have to construct an array of objects, much like the output of ``getRows()``. 65 | Each object will be inserted as a row: 66 | 67 | ```javascript 68 | // Open spreadsheet with ID XXXX-XXXX-XXXX and work with columns A to C in worksheet "Sheet 1" 69 | let sheet = new GoogleSheet({ 70 | sheetId: "XXXXXXXXXX-XXXXXXXXX-XXXXXXXX", 71 | range: "'Sheet 1'!A:C" 72 | }); 73 | 74 | // The data that we want to add to the spreadsheet 75 | let data = [ 76 | { 77 | timestamp: Date.now(), 78 | message: 'Another message', 79 | user: 'Peter' 80 | }, 81 | { 82 | timestamp: Date.now(), 83 | message: 'Awesome work!', 84 | user: 'Simon' 85 | } 86 | ] 87 | 88 | await sheet.appendRows(data); 89 | ``` 90 | 91 | **Note:** the order of the fields doesn't matter. The library will match your data with the header row and insert it in the correct columns. 92 | 93 | **Warning:** the library will throw an error if your data contain a property that isn't in the header row. However, you can omit rows (they will be empty). 94 | 95 | # Contributing & License 96 | Feel free to fork this library, improve it or create issues and pull requests. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-sheets-wrapper", 3 | "version": "1.0.4", 4 | "description": "Lightweight wrapper around the official Google Sheets API that makes it easy to read/write rows", 5 | "keywords": [ 6 | "google sheets", 7 | "typescript" 8 | ], 9 | "main": "index.js", 10 | "scripts": { 11 | "pretest": "tsc", 12 | "test": "tslint **/*.ts --exclude '**/node_modules/**' && mocha tests/" 13 | }, 14 | "author": "Xavier Decuyper ", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@types/node": "^7.0.5", 18 | "google-auth-library": "^0.10.0", 19 | "googleapis": "^17.1.0" 20 | }, 21 | "devDependencies": { 22 | "@types/chai": "^3.4.35", 23 | "@types/mocha": "^2.2.39", 24 | "chai": "^3.5.0", 25 | "mocha": "^3.2.0", 26 | "tslint": "^4.5.1", 27 | "typescript": "^2.2.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/GoogleSheet.ts: -------------------------------------------------------------------------------- 1 | import { RowMapper } from "./RowMapper"; 2 | 3 | export class GoogleSheet { 4 | 5 | // Dependencies 6 | private googleAuth = require("google-auth-library"); 7 | private google = require("googleapis"); 8 | private sheets = this.google.sheets("v4"); 9 | private authFactory = new this.googleAuth(); 10 | private authClient; 11 | private fs = require("fs"); 12 | 13 | // Attributes 14 | private options: IGoogleSheetOptions; 15 | private headerRow: Array; 16 | 17 | constructor(options: IGoogleSheetOptions) { 18 | if (!process.env.GOOGLE_APPLICATION_CREDENTIALS) { 19 | throw new Error("Environment variable 'GOOGLE_APPLICATION_CREDENTIALS' not set."); 20 | } 21 | 22 | if (!this.fs.existsSync(process.env.GOOGLE_APPLICATION_CREDENTIALS)) { 23 | throw new Error("Credentials file does not exist."); 24 | } 25 | 26 | if (!/['"].*['"]![A-Za-z]{1,2}:[A-Za-z]{1,2}/.test(options.range)) { 27 | throw new Error("Range format was invalid. Should be like: 'name-of-sheet'!A:F"); 28 | } 29 | 30 | this.options = options; 31 | } 32 | 33 | /** 34 | * Change the range in which the API should search for data 35 | * @param range 36 | */ 37 | public setRange(range: string) { 38 | // TODO: add some validation here 39 | this.options.range = range; 40 | } 41 | 42 | public async getRows(): Promise { 43 | await this.authenticate(); 44 | 45 | return new Promise((resolve, reject) => { 46 | this.sheets.spreadsheets.values.get({ 47 | auth: this.authClient, 48 | spreadsheetId: this.options.sheetId, 49 | range: this.options.range 50 | }, (err, response) => { 51 | if (err) { reject(err); } 52 | 53 | if (response && response.values) { 54 | resolve(RowMapper.map(response.values)); 55 | } 56 | 57 | reject("Not a valid response: " + response); 58 | }); 59 | }); 60 | } 61 | 62 | 63 | public async appendRows(rows: Array) { 64 | let header: string[] = await this.getHeaderRow(); 65 | let sendToGoogle: Array> = []; 66 | 67 | for (let row of rows) { 68 | 69 | let rowForGoogle = []; 70 | 71 | // Check if all properties are in the header 72 | for (let prop in row) { 73 | let pos = header.indexOf(prop); 74 | 75 | if (pos === -1) { 76 | throw Error(`Property "${prop} does not exist in spreadsheet"`); 77 | } 78 | 79 | rowForGoogle.splice(pos, 0, row[prop]); // Insert them in correct position 80 | } 81 | 82 | sendToGoogle.push(rowForGoogle); 83 | } 84 | 85 | await this.writeToGoogle(sendToGoogle); 86 | } 87 | 88 | public async getHeaderRow(): Promise { 89 | return new Promise(async (resolve, reject) => { 90 | if (this.headerRow) { 91 | resolve(this.headerRow); 92 | } 93 | 94 | let rangeFirstRow = this.options.range.split("!")[0] + "!1:1"; 95 | 96 | 97 | // Go grab it! 98 | await this.authenticate(); 99 | 100 | this.sheets.spreadsheets.values.get({ 101 | auth: this.authClient, 102 | spreadsheetId: this.options.sheetId, 103 | range: rangeFirstRow 104 | }, (err, response) => { 105 | if (err) { reject(err); } 106 | 107 | if (response && response.values) { 108 | this.headerRow = []; 109 | 110 | for (let header of response.values[0]) { 111 | this.headerRow.push( 112 | RowMapper.convertToCamelCase(header) 113 | ); 114 | } 115 | 116 | resolve(this.headerRow); 117 | } 118 | 119 | reject("Not a valid response: " + response); 120 | }); 121 | }); 122 | } 123 | 124 | private async writeToGoogle(values) { 125 | return new Promise((resolve, reject) => { 126 | this.sheets.spreadsheets.values.append({ 127 | auth: this.authClient, 128 | spreadsheetId: this.options.sheetId, 129 | range: this.options.range, 130 | valueInputOption: "USER_ENTERED", 131 | insertDataOption: "INSERT_ROWS", 132 | resource: { 133 | values: values 134 | } 135 | }, (err, response) => { 136 | if (err) { reject(err); } 137 | 138 | if (response && response.updates) { 139 | resolve(response); 140 | } else { 141 | reject("Did not get the expected response: " + response); 142 | } 143 | }); 144 | }); 145 | } 146 | 147 | private async authenticate() { 148 | return new Promise((resolve, reject) => { 149 | 150 | // Skip authentication is it's already done 151 | if (this.authClient) { 152 | resolve(); 153 | } 154 | 155 | this.authFactory.getApplicationDefault((err, authClient) => { 156 | if (err) { 157 | reject("Authentication failed because of " + err); 158 | } 159 | 160 | this.authClient = authClient; 161 | 162 | // Ask for read/write permissions by default 163 | let scopes = ["https://www.googleapis.com/auth/spreadsheets"]; 164 | 165 | if (this.options.readOnly === true) { 166 | scopes = ["https://www.googleapis.com/auth/spreadsheets.readonly"]; 167 | } 168 | 169 | if (authClient.createScopedRequired && authClient.createScopedRequired()) { 170 | this.authClient = authClient.createScoped(scopes); 171 | resolve(); 172 | } 173 | }); 174 | }); 175 | } 176 | } -------------------------------------------------------------------------------- /src/RowMapper.ts: -------------------------------------------------------------------------------- 1 | export class RowMapper { 2 | 3 | static map(input: Array>) { 4 | let header = input.shift(); 5 | 6 | for (let i = 0; i < header.length; i++) { 7 | header[i] = RowMapper.convertToCamelCase(header[i]); 8 | } 9 | 10 | let output = []; 11 | 12 | for (let row of input) { 13 | let obj = {}; 14 | 15 | for (let i = 0; i < row.length; i++) { 16 | obj[header[i]] = row[i]; 17 | } 18 | 19 | output.push(obj); 20 | } 21 | 22 | return output; 23 | } 24 | 25 | static convertToCamelCase(content: string): string { 26 | // content = content.charAt(0).toLowerCase() + content.slice(1); 27 | // content = contennt.replace(/\s/g, ''); 28 | // return content.toLowerCase(); 29 | return content.replace(/[^a-z ]/ig, "").replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { 30 | if (+match === 0) return ""; // or if (/\s+/.test(match)) for white spaces 31 | return index === 0 ? match.toLowerCase() : match.toUpperCase(); 32 | }); 33 | } 34 | } -------------------------------------------------------------------------------- /src/interfaces/IGoogleSheetOptions.ts: -------------------------------------------------------------------------------- 1 | interface IGoogleSheetOptions { 2 | 3 | /** 4 | * ID of the spreadsheet that you want to work with 5 | */ 6 | sheetId: string; 7 | 8 | /** 9 | * Range of your data (eg Where is your data stored) 10 | * Example: 'Sheet1'!A:F fetches column A to F in Sheet 1 11 | */ 12 | range: string; 13 | 14 | /** 15 | * Set to true if you only want to read and deny writes 16 | */ 17 | readOnly?: boolean; 18 | } -------------------------------------------------------------------------------- /src/interfaces/ValueInputOption.ts: -------------------------------------------------------------------------------- 1 | export type ValueInputOption = 2 | "INPUT_VALUE_OPTION_UNSPECIFIED" 3 | | "RAW" 4 | | "USER_ENTERED"; -------------------------------------------------------------------------------- /tests/GoogleSheetTest.ts: -------------------------------------------------------------------------------- 1 | import { GoogleSheet } from "../src/GoogleSheet"; 2 | import "mocha"; 3 | import "chai"; 4 | import { expect } from "chai"; 5 | 6 | describe("GoogleSheet test", () => { 7 | describe("Authentication", () => { 8 | it("Should fail when GOOGLE_APPLICATION_CREDENTIALS is not set", () => { 9 | delete process.env.GOOGLE_APPLICATION_CREDENTIALS; 10 | expect(() => { 11 | new GoogleSheet({ 12 | sheetId: "xxxxxxxxxx", 13 | range: "'Sheet 1'!A:F" 14 | }); 15 | }).to.throw(Error, /not set/); 16 | }); 17 | 18 | it("Should fail when GOOGLE_APPLICATION_CREDENTIALS is set to a non-existing file", () => { 19 | process.env.GOOGLE_APPLICATION_CREDENTIALS = "/tmp/credentials_that_dont_exist.json"; 20 | 21 | expect(() => { 22 | new GoogleSheet({ 23 | sheetId: "xxxxxxxxxx", 24 | range: "'Sheet 1'!A:F" 25 | }); 26 | }).to.throw(Error, /does not exist/); 27 | }); 28 | }); 29 | 30 | // describe("Constructor", () => { 31 | // it("Should fail with wrongly formatted range", (done) => { 32 | // expect(() => { 33 | // new GoogleSheet({ 34 | // sheetId: "xxxxxxx", 35 | // range: "'some:thing'!" 36 | // }); 37 | // }).to.throw(Error, /Range format was invalid/); 38 | // }); 39 | // }); 40 | 41 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": false, 6 | "sourceMap": false 7 | } 8 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsRules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-trailing-whitespace": true, 15 | "no-unsafe-finally": true, 16 | "one-line": [ 17 | true, 18 | "check-open-brace", 19 | "check-whitespace" 20 | ], 21 | "quotemark": [ 22 | true, 23 | "double" 24 | ], 25 | "semicolon": [ 26 | true, 27 | "always" 28 | ], 29 | "triple-equals": [ 30 | true, 31 | "allow-null-check" 32 | ], 33 | "variable-name": [ 34 | true, 35 | "ban-keywords" 36 | ], 37 | "whitespace": [ 38 | true, 39 | "check-branch", 40 | "check-decl", 41 | "check-operator", 42 | "check-separator", 43 | "check-type" 44 | ] 45 | }, 46 | "rules": { 47 | "class-name": true, 48 | "comment-format": [ 49 | true, 50 | "check-space" 51 | ], 52 | "indent": [ 53 | true, 54 | "spaces" 55 | ], 56 | "no-eval": true, 57 | "no-internal-module": true, 58 | "no-trailing-whitespace": true, 59 | "no-unsafe-finally": true, 60 | "no-var-keyword": true, 61 | "one-line": [ 62 | true, 63 | "check-open-brace", 64 | "check-whitespace" 65 | ], 66 | "quotemark": [ 67 | true, 68 | "double" 69 | ], 70 | "semicolon": [ 71 | true, 72 | "always" 73 | ], 74 | "triple-equals": [ 75 | true, 76 | "allow-null-check" 77 | ], 78 | "typedef-whitespace": [ 79 | true, 80 | { 81 | "call-signature": "nospace", 82 | "index-signature": "nospace", 83 | "parameter": "nospace", 84 | "property-declaration": "nospace", 85 | "variable-declaration": "nospace" 86 | } 87 | ], 88 | "variable-name": [ 89 | true, 90 | "ban-keywords" 91 | ], 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ] 100 | } 101 | } --------------------------------------------------------------------------------