├── .jshintrc
├── .npmignore
├── index.js
├── .gitignore
├── example
├── cred-loader.js
├── edit-basic.js
├── edit-basic-async-await.js
├── edit-autosize.js
├── read-basic-async-await.js
├── edit-metadata.js
├── read-basic.js
├── read-metadata.js
└── edit-cell-names.js
├── package.json
├── lib
├── auth.js
├── metadata.js
├── util.js
└── spreadsheet.js
├── get_oauth2_permissions.js
├── test
└── spreadsheet.js
└── README.md
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node" : true
3 | }
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | test/
3 | example/
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/spreadsheet');
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | example/creds.json
3 | example/oauth2-creds.json
4 | browseroauth2/
--------------------------------------------------------------------------------
/example/cred-loader.js:
--------------------------------------------------------------------------------
1 | //load example credential file
2 | //in production, use enviroment variables instead
3 | try {
4 | module.exports = require("./oauth2-creds.json");
5 | } catch (err) {
6 | console.log(
7 | 'Please make a "oauth2-creds.json" file in this folder to use these examples.'
8 | );
9 | console.log(
10 | '{ "client_id": "...", "client_secret": "...", "refresh_token": "..." }'
11 | );
12 | console.log("To retrieve this token, see ../get_oauth2_permissions.js");
13 | process.exit(1);
14 | }
15 |
--------------------------------------------------------------------------------
/example/edit-basic.js:
--------------------------------------------------------------------------------
1 | var Spreadsheet = require("../");
2 |
3 | Spreadsheet.load(
4 | {
5 | debug: true,
6 | oauth2: require("./cred-loader"),
7 | spreadsheetName: "edit-spreadsheet-example",
8 | worksheetName: "Sheet1"
9 | },
10 | function run(err, spreadsheet) {
11 | if (err) throw err;
12 | //insert 'hello!' at E3
13 | spreadsheet.add({3: {5: "hello!"}});
14 |
15 | spreadsheet.send(function(err) {
16 | if (err) throw err;
17 | console.log("Updated Cell at row 3, column 5 to 'hello!'");
18 | });
19 | }
20 | );
21 |
--------------------------------------------------------------------------------
/example/edit-basic-async-await.js:
--------------------------------------------------------------------------------
1 | const Spreadsheet = require("../");
2 | const creds = require("./cred-loader");
3 |
4 | (async () => {
5 | try {
6 | let spreadsheet = await Spreadsheet.load({
7 | debug: true,
8 | oauth2: creds,
9 | spreadsheetName: "node-spreadsheet-example",
10 | worksheetName: "Sheet1"
11 | });
12 | //insert 'Zip zop' at E3
13 | spreadsheet.add({3: {5: "Zip zop"}});
14 | await spreadsheet.send();
15 | console.log("Updated Cell E3");
16 | } catch (err) {
17 | console.error("EROR", err);
18 | }
19 | })();
20 |
--------------------------------------------------------------------------------
/example/edit-autosize.js:
--------------------------------------------------------------------------------
1 | var Spreadsheet = require("../");
2 |
3 | Spreadsheet.load(
4 | {
5 | debug: true,
6 | oauth2: require("./cred-loader"),
7 | spreadsheetName: "edit-spreadsheet-example",
8 | worksheetName: "Sheet1"
9 | },
10 | function run(err, spreadsheet) {
11 | if (err) throw err;
12 | spreadsheet.add({300: {50: "hello!"}});
13 | spreadsheet.send({autoSize: true}, function(err) {
14 | if (err) throw err;
15 | console.log(
16 | "Resized then updated Cell at row 300, column 50 to 'hello!'"
17 | );
18 | });
19 | }
20 | );
21 |
--------------------------------------------------------------------------------
/example/read-basic-async-await.js:
--------------------------------------------------------------------------------
1 | const Spreadsheet = require("../");
2 | const creds = require("./cred-loader");
3 |
4 | (async () => {
5 | try {
6 | let spreadsheet = await Spreadsheet.load({
7 | debug: true,
8 | oauth2: creds,
9 | spreadsheetName: "node-spreadsheet-example",
10 | worksheetName: "Sheet1"
11 | });
12 | //receive all cells
13 | let [rows, info] = await spreadsheet.receive({getValues: false});
14 | console.log("Found rows:", rows);
15 | console.log("With info:", info);
16 | } catch (err) {
17 | console.error("EROR", err);
18 | }
19 | })();
20 |
--------------------------------------------------------------------------------
/example/edit-metadata.js:
--------------------------------------------------------------------------------
1 | var Spreadsheet = require("../");
2 | var util = require("util");
3 |
4 | Spreadsheet.load(
5 | {
6 | debug: true,
7 | oauth2: require("./cred-loader"),
8 | spreadsheetName: "edit-spreadsheet-example",
9 | worksheetName: "Sheet1"
10 | },
11 | function run(err, spreadsheet) {
12 | if (err) throw err;
13 | spreadsheet.metadata(
14 | {
15 | title: "Sheet1",
16 | rowCount: 5,
17 | colCount: 5
18 | },
19 | function(err, metadata) {
20 | if (err) throw err;
21 | console.log(metadata);
22 | }
23 | );
24 | }
25 | );
26 |
--------------------------------------------------------------------------------
/example/read-basic.js:
--------------------------------------------------------------------------------
1 | var Spreadsheet = require("../");
2 | var creds = require("./cred-loader");
3 |
4 | Spreadsheet.load(
5 | {
6 | debug: true,
7 | oauth2: require("./cred-loader"),
8 | spreadsheetName: "node-edit-spreadsheet",
9 | worksheetName: "Sheet1"
10 | },
11 | function run(err, spreadsheet) {
12 | if (err) return console.log(err);
13 | //receive all cells
14 | spreadsheet.receive({getValues: false}, function(err, rows, info) {
15 | if (err) return console.log(err);
16 | console.log("Found rows:", rows);
17 | console.log("With info:", info);
18 | });
19 | }
20 | );
21 |
--------------------------------------------------------------------------------
/example/read-metadata.js:
--------------------------------------------------------------------------------
1 | var Spreadsheet = require("../");
2 | var creds = require("./cred-loader");
3 | var util = require("util");
4 |
5 | Spreadsheet.load(
6 | {
7 | debug: true,
8 | oauth2: require("./cred-loader"),
9 | spreadsheetName: "edit-spreadsheet-example",
10 | worksheetName: "Sheet1"
11 | },
12 | function run(err, spreadsheet) {
13 | if (err) throw err;
14 | spreadsheet.metadata(function(err, metadata) {
15 | if (err) throw err;
16 | console.log(metadata);
17 | // { title: 'Sheet1', rowCount: '100', colCount: '20',
18 | // updated: Sun Jul 28 2013 12:07:31 GMT+1000 (EST) }
19 | });
20 | }
21 | );
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "edit-google-spreadsheet",
3 | "version": "0.3.0",
4 | "scripts": {
5 | "test": "mocha test --recursive"
6 | },
7 | "dependencies": {
8 | "google-auth-library": "^0.9.6",
9 | "async": "^2.5.0",
10 | "colors": "^1.1.2",
11 | "google-oauth-jwt": "^0.2.0",
12 | "lodash": "^4.17.4",
13 | "request": "^2.82.0",
14 | "xml2js": "^0.4.19"
15 | },
16 | "repository": "git://github.com/jpillora/node-edit-google-spreadsheet.git",
17 | "devDependencies": {
18 | "chai": "^3.3.0",
19 | "mocha": "^2.3.3",
20 | "mockery": "^1.4.0",
21 | "sinon": "^1.17.1",
22 | "sinon-chai": "^2.8.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example/edit-cell-names.js:
--------------------------------------------------------------------------------
1 | var Spreadsheet = require("../");
2 |
3 | Spreadsheet.load(
4 | {
5 | debug: true,
6 | oauth2: require("./cred-loader"),
7 | spreadsheetName: "edit-spreadsheet-example",
8 | worksheetName: "Sheet1"
9 | },
10 | function run(err, spreadsheet) {
11 | if (err) throw err;
12 | spreadsheet.add({
13 | 3: {
14 | 4: {name: "a", val: 42}, //'42' though tagged as "a"
15 | 5: {name: "b", val: 21}, //'21' though tagged as "b"
16 | 6: "={{ a }}+{{ b }}" //forumla adding row3,col4 with row3,col5 => '=D3+E3'
17 | }
18 | });
19 | spreadsheet.send(function(err) {
20 | if (err) throw err;
21 | console.log("Cells updated");
22 | });
23 | }
24 | );
25 |
--------------------------------------------------------------------------------
/lib/auth.js:
--------------------------------------------------------------------------------
1 | var GoogleOAuthJWT = require("google-oauth-jwt");
2 | var GoogleAuth = require("google-auth-library");
3 | var googleAuth = new GoogleAuth();
4 |
5 | //client auth helper
6 | module.exports = function(opts, callback) {
7 | if (opts.username && opts.password) callback("Client login not supported");
8 | else if (opts.oauth) oAuth1Login(opts.oauth, opts.useHTTPS, callback);
9 | else if (opts.oauth2) oAuth2Login(opts.oauth2, callback);
10 | else if (opts.accessToken) accessTokenLogin(opts.accessToken, callback);
11 | };
12 |
13 | function oAuth1Login(oauth, useHTTPS, callback) {
14 | if (!oauth.scopes) {
15 | oauth.scopes = ["http" + useHTTPS + "://spreadsheets.google.com/feeds"];
16 | }
17 | GoogleOAuthJWT.authenticate(oauth, function(err, token) {
18 | if (err) callback("Google OAuth Error: " + err);
19 | else callback(null, {type: "Bearer", token: token});
20 | });
21 | }
22 |
23 | function oAuth2Login(oauth2, callback) {
24 | var oAuth2Client = new googleAuth.OAuth2(
25 | oauth2.client_id,
26 | oauth2.client_secret,
27 | "urn:ietf:wg:oauth:2.0:oob"
28 | );
29 |
30 | oAuth2Client.setCredentials({
31 | access_token: "DUMMY",
32 | expiry_date: 1,
33 | refresh_token: oauth2.refresh_token,
34 | token_type: "Bearer"
35 | });
36 | oAuth2Client.getAccessToken(function(err, token) {
37 | if (err) callback("Google OAuth2 Error: " + err);
38 | else callback(null, {type: "Bearer", token: token});
39 | });
40 | }
41 |
42 | function accessTokenLogin(accessToken, callback) {
43 | function gotToken(err, t) {
44 | if (err) return callback(err);
45 | if (!t.type || !t.token)
46 | return callback("Missing token or token type information");
47 | //got token
48 | callback(null, {type: t.type, token: t.token});
49 | }
50 | //
51 | if (typeof accessToken === "function") accessToken(gotToken);
52 | else if (typeof accessToken === "object") gotToken(null, accessToken);
53 | else callback("Invalid access token");
54 | }
55 |
--------------------------------------------------------------------------------
/lib/metadata.js:
--------------------------------------------------------------------------------
1 | var request = require("request");
2 | var _ = require("lodash");
3 |
4 | var Metadata = module.exports = function(spreadsheet){
5 | this.spreadsheet = spreadsheet;
6 | };
7 |
8 | Metadata.prototype.url = function(isId) {
9 | return this.spreadsheet.protocol+'://spreadsheets.google.com/feeds/worksheets/' +
10 | this.spreadsheet.spreadsheetId + '/' + (isId?'':'private/full/') + this.spreadsheet.worksheetId;
11 | };
12 |
13 | Metadata.prototype.extract = function(result) {
14 | return {
15 | updated: new Date(result.entry.updated),
16 | title: result.entry.title,
17 | rowCount: result.entry['gs:rowCount'],
18 | colCount: result.entry['gs:colCount']
19 | };
20 | };
21 |
22 | Metadata.prototype.get = function(callback) {
23 | var _this = this;
24 | this.spreadsheet.request({
25 | url: this.url()
26 | }, function(err, result) {
27 | if(err) return callback(err);
28 | callback(null, _this.extract(result));
29 | });
30 | };
31 |
32 | Metadata.prototype.set = function(data, callback) {
33 | var _this = this;
34 | //must retrieve current col/row counts if missing
35 | if(data.colCount === undefined || data.rowCount === undefined)
36 | this.get(function(err, metadata) {
37 | if(err) return callback(err);
38 | _this._set(_.extend(metadata, data), callback);
39 | });
40 | else
41 | _this._set(data, callback);
42 | };
43 |
44 | Metadata.prototype._set = function(data, callback) {
45 | var _this = this;
46 | var entry = ''+
48 | ''+this.url(true)+'' +
49 | ''+data.title+'' +
50 | ''+data.colCount+'' +
51 | ''+data.rowCount+'' +
52 | '';
53 |
54 | this.spreadsheet.request({
55 | method: 'PUT',
56 | url: this.url(),
57 | body: entry
58 | }, function(err, result) {
59 | if(err) return callback(err);
60 | callback(null, _this.extract(result));
61 | });
62 | };
63 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 |
2 | //parse number
3 | exports.num = function(obj) {
4 | if (obj === undefined) return 0;
5 | if (typeof obj === "number" || typeof obj === "boolean") return obj;
6 | if (typeof obj === "string") {
7 | //ensure that the string is *only* a number
8 | if (!/^\-?\d+(\.\d+)?$/.test(obj)) return obj;
9 | var res = parseFloat(obj, 10);
10 | if (isNaN(res)) return obj;
11 | return res;
12 | }
13 | throw "Invalid number: " + JSON.stringify(obj);
14 | };
15 |
16 | exports.int2cell = function(r, c) {
17 | return String.fromCharCode(64 + c) + r;
18 | };
19 |
20 | exports.gcell2cell = function(cell, getValue, useTextValues) {
21 | //get formula AND has a formula?
22 | if (!getValue && /^=/.test(cell.inputValue)) {
23 | //must convert '=RC[-2]+R[3]C[-1]' to '=B5+C8'
24 | return cell.inputValue.replace(
25 | /(R(\[(-?\d+)\])?)(C(\[(-?\d+)\])?)/g,
26 | function() {
27 | return exports.int2cell(
28 | exports.num(cell.row) + exports.num(arguments[3] || 0),
29 | exports.num(cell.col) + exports.num(arguments[6] || 0)
30 | );
31 | }
32 | );
33 | }
34 | //get value
35 | return useTextValues ? exports.num(cell.$t) : exports.num(cell.inputValue);
36 | };
37 |
38 | exports.promisify = function(fn) {
39 | return function wrapped() {
40 | //store promise on callback
41 | var resolve, reject;
42 | var promise = new Promise(function(res, rej) {
43 | resolve = res;
44 | reject = rej;
45 | });
46 | //
47 | var args = Array.from(arguments);
48 | var callback = args[args.length - 1];
49 | var hasCallback = typeof callback === "function";
50 | //resolve/reject promise
51 | var fullfilled = function(err, data) {
52 | if (err) {
53 | reject(err);
54 | } else {
55 | var datas = Array.prototype.slice.call(arguments, 1);
56 | if (datas.length >= 2) {
57 | resolve(datas);
58 | } else {
59 | resolve(data);
60 | }
61 | }
62 | if (hasCallback) {
63 | callback.apply(this, arguments);
64 | }
65 | };
66 | //replace/add callback
67 | if (hasCallback) {
68 | args[args.length - 1] = fullfilled;
69 | } else {
70 | args.push(fullfilled);
71 | }
72 | //call underlying function
73 | var returned = fn.apply(this, args);
74 | if (typeof returned !== "undefined") {
75 | console.log("promisify warning: discarded return value");
76 | }
77 | //return promise!
78 | return promise;
79 | };
80 | };
81 |
--------------------------------------------------------------------------------
/get_oauth2_permissions.js:
--------------------------------------------------------------------------------
1 | // Based on https://raw.githubusercontent.com/google/google-api-nodejs-client/master/examples/oauth2.js
2 | // Adapted by Oystein Steimler
3 |
4 | /**
5 | * Copyright 2012 Google Inc. All Rights Reserved.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | */
19 |
20 | var readline = require("readline");
21 |
22 | var googleAuth = require("google-auth-library");
23 | var OAuth2Client = new googleAuth().OAuth2;
24 |
25 | // Client ID and client secret are available at
26 | // https://code.google.com/apis/console
27 | // 1. Create or pick a project
28 | // 2. Choose "API & Auth" and then "Credentials"
29 | // 3. Click "Create new Client ID"
30 | // 4. Select "Installed application" and "Other"
31 | // 5. Copy your ClientID and Client Secret into the fields beneath
32 |
33 | var CLIENT_ID = '';
34 | var CLIENT_SECRET = '';
35 |
36 | // 6. This scope 'https://spreadsheets.google.com/feeds' provides full access to all Spreadsheets in
37 | // your Google Drive. Find more scopes here: https://developers.google.com/drive/web/scopes
38 | // and https://developers.google.com/google-apps/spreadsheets/authorize
39 | var PERMISSION_SCOPE = "https://spreadsheets.google.com/feeds"; //space-delimited string or an array of scopes
40 |
41 | // 6. Run this script: `node get_oauth2_permissions.js'
42 | // 7. Visit the URL printed, authenticate the google user, grant the permission
43 | // 8. Copy the authorization code and paste it at the prompt of this program.
44 | // 9. The refresh_token you get is needed with the client_id and client_secret when using edit-google-spreadsheet
45 |
46 | var oauth2Client = new OAuth2Client(
47 | CLIENT_ID,
48 | CLIENT_SECRET,
49 | "urn:ietf:wg:oauth:2.0:oob"
50 | );
51 |
52 | var rl = readline.createInterface({
53 | input: process.stdin,
54 | output: process.stdout
55 | });
56 |
57 | // generate consent page url
58 | var url = oauth2Client.generateAuthUrl({
59 | access_type: "offline",
60 | scope: PERMISSION_SCOPE
61 | });
62 |
63 | console.log("Visit this url:\n%s\n", url); // provides a refresh token
64 |
65 | rl.question("Enter the code here: ", function(code) {
66 | console.log("\nGetting token...");
67 | oauth2Client.getToken(code, function(err, tokens) {
68 | if (err) return console.log("Error getting token: " + err);
69 | var creds = {
70 | client_id: CLIENT_ID,
71 | client_secret: CLIENT_SECRET,
72 | refresh_token: tokens.refresh_token
73 | };
74 | console.log(
75 | 'Use this in your Spreadsheet.load():\n"oauth2": %s',
76 | JSON.stringify(creds, true, 2)
77 | );
78 | process.exit(0);
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/test/spreadsheet.js:
--------------------------------------------------------------------------------
1 | var chai = require('chai');
2 | var sinon = require('sinon');
3 | var sinonChai = require('sinon-chai');
4 | chai.use(sinonChai);
5 | var expect = chai.expect;
6 | var mockery = require('mockery');
7 |
8 | var mockRequestCbParams = {
9 | err: null,
10 | response: { statusCode: 200 },
11 | body: {}
12 | };
13 | var mockRequest = sinon.spy(function(opts, callback) {
14 | callback(
15 | mockRequestCbParams.err,
16 | mockRequestCbParams.response,
17 | mockRequestCbParams.body
18 | );
19 | });
20 |
21 | var mockAuth = sinon.spy(function(opts, callback) {
22 | if (mockRequestCbParams.response.statusCode == 401) {
23 | // we're authed now...
24 | mockRequestCbParams.response.statusCode = 200;
25 | }
26 | callback(null, 'mockToken');
27 | });
28 |
29 | var opts = {
30 | oauth2: 'oauth2',
31 | spreadsheetId: 'spreadsheetId',
32 | worksheetId: 'worksheetId'
33 | };
34 |
35 |
36 | var Spreadsheet;
37 |
38 | describe('Spreadsheet', function() {
39 | before(function() {
40 |
41 | mockery.enable({warnOnUnregistered: false});
42 |
43 | mockery.registerMock('request', mockRequest);
44 | mockery.registerMock('./auth', mockAuth);
45 |
46 | Spreadsheet = require('../lib/spreadsheet.js');
47 | });
48 | after(function() {
49 | mockery.deregisterAll();
50 | mockery.disable();
51 | });
52 |
53 | describe('create (alias load)', function() {
54 | it('should call the callback', function() {
55 | var callback = sinon.spy();
56 | Spreadsheet.create({}, callback);
57 | expect(callback).to.have.been.calledOnce;
58 | });
59 | it('should call the opts.callback', function() {
60 | var callback = sinon.spy();
61 | Spreadsheet.create({callback: callback});
62 | expect(callback).to.have.been.calledOnce;
63 | });
64 | it('should throw an error if there is no callback', function() {
65 | expect(function() {
66 | Spreadsheet.create({});
67 | }).to.throw('Missing callback')
68 | });
69 | it('should return an error if there is no auth mechanism', function() {
70 | var errValue;
71 | var callback = sinon.spy(function(err) {errValue = err;});
72 | Spreadsheet.create({spreadsheetId: 'spreadsheetId', worksheetId: 'worksheetId'}, callback);
73 | expect(callback).to.have.been.calledOnce;
74 | expect(errValue).to.equal('Missing authentication information');
75 | });
76 | it('should return an error if there is no spreadsheet specified', function() {
77 | var errValue;
78 | var callback = sinon.spy(function(err) {errValue = err;});
79 | Spreadsheet.create({oauth2: 'oauth2', worksheetId: 'worksheetId'}, callback);
80 | expect(callback).to.have.been.calledOnce;
81 | expect(errValue).to.equal("Missing 'spreadsheetId' or 'spreadsheetName'");
82 | });
83 | it('should return an error if there is no worksheet specified', function() {
84 | var errValue;
85 | var callback = sinon.spy(function(err) {errValue = err;});
86 | Spreadsheet.create({oauth2: 'oauth2', spreadsheetId: 'spreadsheetId'}, callback);
87 | expect(callback).to.have.been.calledOnce;
88 | expect(errValue).to.equal("Missing 'worksheetId' or 'worksheetName'");
89 | });
90 | describe('when authing with defaults', function() {
91 | var authParams;
92 | before(function(done) {
93 | mockAuth.reset();
94 | Spreadsheet.create(opts, function() {
95 | authParams = mockAuth.args[0][0];
96 | done();
97 | });
98 | });
99 | it('should default to use cell text values', function() {
100 | expect(authParams.useCellTextValues).to.be.true;
101 | });
102 | it('should default to https', function() {
103 | expect(authParams.useHTTPS).to.equal('s');
104 | });
105 | it('should pass the specified parameters', function() {
106 | expect(authParams.oauth2).to.equal('oauth2');
107 | expect(authParams.spreadsheetId).to.equal('spreadsheetId');
108 | expect(authParams.worksheetId).to.equal('worksheetId');
109 | });
110 | });
111 | it('should be able to override use cell text values', function(done) {
112 | mockAuth.reset();
113 | var opts = {
114 | oauth2: 'oauth2',
115 | spreadsheetId: 'spreadsheetId',
116 | worksheetId: 'worksheetId',
117 | useCellTextValues: false
118 | };
119 | Spreadsheet.create(opts, function() {
120 | expect(mockAuth.args[0][0].useCellTextValues).to.be.false;
121 | done();
122 | });
123 | });
124 | it('should be able to override use HTTPS', function(done) {
125 | mockAuth.reset();
126 | var opts = {
127 | oauth2: 'oauth2',
128 | spreadsheetId: 'spreadsheetId',
129 | worksheetId: 'worksheetId',
130 | useHTTPS: false
131 | };
132 | Spreadsheet.create(opts, function() {
133 | expect(mockAuth.args[0][0].useHTTPS).to.be.empty;
134 | done();
135 | });
136 | });
137 | it('should pass back an initialized spreadsheet instance', function(done) {
138 | Spreadsheet.create(opts, function(err, spreadsheet) {
139 | expect(err).to.not.exist;
140 | expect(spreadsheet.spreadsheetId).to.equal('spreadsheetId');
141 | expect(spreadsheet.worksheetId).to.equal('worksheetId');
142 | expect(spreadsheet.protocol).to.equal('https');
143 | done();
144 | });
145 | });
146 | });
147 |
148 | describe('spreadsheet', function() {
149 | var spreadsheet;
150 |
151 | before(function(done) {
152 | Spreadsheet.create(opts, function(err, createdSheet) {
153 | spreadsheet = createdSheet;
154 | done();
155 | });
156 | });
157 |
158 | describe('request', function() {
159 | var opts = {
160 | url: 'https://example.com'
161 | };
162 | beforeEach(function() {
163 | mockRequestCbParams = {
164 | err: null,
165 | response: { statusCode: 200 },
166 | body: '',
167 | headers: { 'content-type': 'application/atom+xml; charset=UTF-8; type=feed' }
168 | };
169 | });
170 | it('should return an error if no URL is provided', function(done) {
171 | spreadsheet.request({}, function(err, response) {
172 | expect(err).to.equal('Invalid request');
173 | done();
174 | });
175 | });
176 | it('should return an error if the request fails (ie. timeout)', function(done) {
177 | mockRequestCbParams.err = new Error('ETIMEDOUT');
178 | spreadsheet.request(opts, function(err, response) {
179 | expect(err).to.be.an.instanceof(Error);
180 | done();
181 | });
182 | });
183 | it('should return an error if the server does not respond', function(done) {
184 | mockRequestCbParams.response = undefined;
185 | spreadsheet.request(opts, function(err, response) {
186 | expect(err).to.equal('no response');
187 | done();
188 | });
189 | });
190 | it('should return an error if the server returns an error', function(done) {
191 | mockRequestCbParams.response.statusCode = 500;
192 | mockRequestCbParams.body = 'Something broke.';
193 | spreadsheet.request(opts, function(err, response) {
194 | expect(err).to.equal('Something broke.');
195 | done();
196 | });
197 | });
198 | it('should reauth if the server returns a 401 (Unauthorized)', function(done) {
199 | mockAuth.reset();
200 | mockRequestCbParams.response.statusCode = 401;
201 | spreadsheet.request(opts, function(err, response) {
202 | expect(err).to.not.exist;
203 | expect(mockAuth).to.have.been.calledOnce;
204 | done();
205 | });
206 | });
207 | it('should parse text elements', function(done) {
208 | mockRequestCbParams.body = 'Income';
209 | spreadsheet.request(opts, function(err, response) {
210 | expect(response.title).to.equal('Income');
211 | done();
212 | });
213 | });
214 | it('should coerce numeric integer elements', function(done) {
215 | mockRequestCbParams.body = '45';
216 | spreadsheet.request(opts, function(err, response) {
217 | expect(response['gs:rowCount']).to.equal(45);
218 | done();
219 | });
220 | });
221 | it('should coerce numeric floating elements', function(done) {
222 | mockRequestCbParams.body = "1.20";
223 | spreadsheet.request(opts, function(err, response) {
224 | expect(response['gs:cell'].$t).to.equal(1.2);
225 | done();
226 | });
227 | });
228 | it('should parse text attributes', function(done) {
229 | mockRequestCbParams.body = "";
230 | spreadsheet.request(opts, function(err, response) {
231 | expect(response['batch:status'].reason).to.equal('Success');
232 | done();
233 | });
234 | });
235 | it('should coerce numeric integer attributes', function(done) {
236 | mockRequestCbParams.body = "";
237 | spreadsheet.request(opts, function(err, response) {
238 | expect(response['batch:status'].code).to.equal(200);
239 | expect(response['batch:status'].code).to.be.a('number');
240 | done();
241 | });
242 | });
243 | it('should coerce numeric float attributes', function(done) {
244 | mockRequestCbParams.body = "1.20";
245 | spreadsheet.request(opts, function(err, response) {
246 | expect(response['gs:cell'].numericValue).to.equal(1.2);
247 | expect(response['gs:cell'].numericValue).to.be.a('number');
248 | done();
249 | });
250 | });
251 | });
252 | });
253 | });
254 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED
2 |
3 | :warning: This module has been deprecated.
4 |
5 | Instead of upgrading this old Google Spreadsheets module to support the Google Sheets v4 API, I've chosen to deprecate it, and provide a guide on how to use the `googleapis` module directly. The `googleapis` module, along with the Sheets v4 API provides:
6 |
7 | - Faster responses
8 | - More features
9 | - Uses JSON instead of XML
10 | - `async`/`await` support
11 |
12 | Please see this new repository for more information
13 |
14 | ### https://github.com/jpillora/node-google-sheets
15 |
16 | ---
17 |
18 | ## Edit Google Spreadsheet
19 |
20 | > A simple API for reading and writing Google Spreadsheets in Node.js
21 |
22 | This module aims to be a complete wrapper around the [Google Sheets API version 3.0](https://developers.google.com/google-apps/spreadsheets/). If anything is missing, create an issue, or even better, a pull request.
23 |
24 | [](https://npmjs.org/package/edit-google-spreadsheet)
25 |
26 | [](https://david-dm.org/jpillora/node-edit-google-spreadsheet)
27 |
28 | :warning: Google has finally deprecated ClientLogin, which means you can no longer authenticate with your email and password. See https://github.com/jpillora/node-edit-google-spreadsheet/issues/72 for updates.
29 |
30 | #### Install
31 |
32 | ```
33 | npm install edit-google-spreadsheet
34 | ```
35 |
36 | #### Basic Usage
37 |
38 | Load a spreadsheet:
39 |
40 | ```js
41 | var Spreadsheet = require("edit-google-spreadsheet");
42 |
43 | Spreadsheet.load(
44 | {
45 | debug: true,
46 | spreadsheetName: "edit-spreadsheet-example",
47 | worksheetName: "Sheet1",
48 |
49 | // Choose from 1 of the 5 authentication methods:
50 |
51 | // 1. Username and Password has been deprecated. OAuth2 is recommended.
52 |
53 | // OR 2. OAuth
54 | oauth: {
55 | email: "my-name@google.email.com",
56 | keyFile: "my-private-key.pem"
57 | },
58 |
59 | // OR 3. OAuth2 (See get_oauth2_permissions.js)
60 | oauth2: {
61 | client_id: "generated-id.apps.googleusercontent.com",
62 | client_secret: "generated-secret",
63 | refresh_token: "token generated with get_oauth2_permission.js"
64 | },
65 |
66 | // OR 4. Static Token
67 | accessToken: {
68 | type: "Bearer",
69 | token: "my-generated-token"
70 | },
71 |
72 | // OR 5. Dynamic Token
73 | accessToken: function(callback) {
74 | //... async stuff ...
75 | callback(null, token);
76 | }
77 | },
78 | function sheetReady(err, spreadsheet) {
79 | //use speadsheet!
80 | }
81 | );
82 | ```
83 |
84 | _Note: Using the options `spreadsheetName` and `worksheetName` will cause lookups for `spreadsheetId` and `worksheetId`. Use `spreadsheetId` and `worksheetId` for improved performance._
85 |
86 | Update sheet:
87 |
88 | ```js
89 | function sheetReady(err, spreadsheet) {
90 | if (err) throw err;
91 |
92 | spreadsheet.add({ 3: { 5: "hello!" } });
93 |
94 | spreadsheet.send(function(err) {
95 | if (err) throw err;
96 | console.log("Updated Cell at row 3, column 5 to 'hello!'");
97 | });
98 | }
99 | ```
100 |
101 | Read sheet:
102 |
103 | ```js
104 | function sheetReady(err, spreadsheet) {
105 | if (err) throw err;
106 |
107 | spreadsheet.receive(function(err, rows, info) {
108 | if (err) throw err;
109 | console.log("Found rows:", rows);
110 | // Found rows: { '3': { '5': 'hello!' } }
111 | });
112 | }
113 | ```
114 |
115 | #### `async` / `await` Usage
116 |
117 | All functions which have a `callback` return a `Promise` tied
118 | to that callback and can therefore be used with `async` / `await`.
119 |
120 | ```js
121 |
122 | const Spreadsheet = require("../");
123 |
124 | (async function example() {
125 | let spreadsheet = await Spreadsheet.load({
126 | debug: true,
127 | oauth2: ...,
128 | spreadsheetName: "node-spreadsheet-example",
129 | worksheetName: "Sheet1"
130 | });
131 | //receive all cells
132 | let [rows, info] = await spreadsheet.receive({getValues: false});
133 | console.log("Found rows:", rows);
134 | console.log("With info:", info);
135 | })().catch;
136 | ```
137 |
138 | #### Metadata
139 |
140 | Get metadata
141 |
142 | ```js
143 | function sheetReady(err, spreadsheet) {
144 | if (err) throw err;
145 |
146 | spreadsheet.metadata(function(err, metadata) {
147 | if (err) throw err;
148 | console.log(metadata);
149 | // { title: 'Sheet3', rowCount: '100', colCount: '20', updated: [Date] }
150 | });
151 | }
152 | ```
153 |
154 | Set metadata
155 |
156 | ```js
157 | function sheetReady(err, spreadsheet) {
158 | if(err) throw err;
159 |
160 | spreadsheet.metadata({
161 | title: 'Sheet2'
162 | rowCount: 100,
163 | colCount: 20
164 | }, function(err, metadata){
165 | if(err) throw err;
166 | console.log(metadata);
167 | });
168 | }
169 | ```
170 |
171 | **_WARNING: all cells outside the range of the new size will be silently deleted_**
172 |
173 | #### More `add` Examples
174 |
175 | Batch edit:
176 |
177 | ```js
178 | spreadsheet.add([[1, 2, 3], [4, 5, 6]]);
179 | ```
180 |
181 | Batch edit starting from row 5:
182 |
183 | ```js
184 | spreadsheet.add({
185 | 5: [[1, 2, 3], [4, 5, 6]]
186 | });
187 | ```
188 |
189 | Batch edit starting from row 5, column 7:
190 |
191 | ```js
192 | spreadsheet.add({
193 | 5: {
194 | 7: [[1, 2, 3], [4, 5, 6]]
195 | }
196 | });
197 | ```
198 |
199 | Formula building with named cell references:
200 |
201 | ```js
202 | spreadsheet.add({
203 | 3: {
204 | 4: { name: "a", val: 42 }, //'42' though tagged as "a"
205 | 5: { name: "b", val: 21 }, //'21' though tagged as "b"
206 | 6: "={{ a }}+{{ b }}" //forumla adding row3,col4 with row3,col5 => '=D3+E3'
207 | }
208 | });
209 | ```
210 |
211 | _Note: cell `a` and `b` are looked up on `send()`_
212 |
213 | #### API
214 |
215 | ##### `Spreadsheet.load( options, callback( err, spreadsheet ) )`
216 |
217 | See [Options](https://github.com/jpillora/node-edit-google-spreadsheet#options) below
218 |
219 | ##### spreadsheet.`add( obj | array )`
220 |
221 | Add cells to the batch. See examples.
222 |
223 | ##### spreadsheet.`send( [options,] callback( err ) )`
224 |
225 | Sends off the batch of `add()`ed cells. Clears all cells once complete.
226 |
227 | `options.autoSize` When required, increase the worksheet size (rows and columns) in order to fit the batch - _NOTE: When enabled, this will trigger an extra request on every `send()`_ (default `false`).
228 |
229 | ##### spreadsheet.`receive( [options,] callback( err , rows , info ) )`
230 |
231 | Recieves the entire spreadsheet. The `rows` object is an object in the same format as the cells you `add()`, so `add(rows)` will be valid. The `info` object looks like:
232 |
233 | ```
234 | {
235 | spreadsheetId: 'ttFmrFPIipJimDQYSFyhwTg',
236 | worksheetId: 'od6',
237 | worksheetTitle: 'Sheet1',
238 | worksheetUpdated: '2013-05-31T11:38:11.116Z',
239 | authors: [ { name: 'jpillora', email: 'dev@jpillora.com' } ],
240 | totalCells: 1,
241 | totalRows: 1,
242 | lastRow: 3
243 | }
244 | ```
245 |
246 | `options.getValues` Always get the values (results) of forumla cells.
247 |
248 | ##### spreadsheet.`metadata( [data, ] callback )`
249 |
250 | Get and set metadata
251 |
252 | _Note: when setting new metadata, if `rowCount` and/or `colCount` is left out,
253 | an extra request will be made to retrieve the missing data._
254 |
255 | ##### spreadsheet.`raw`
256 |
257 | The raw data recieved from Google when enumerating the spreedsheet and worksheet lists, _which are triggered when searching for IDs_. In order to see this array of all spreadsheets (`raw.spreadsheets`) the `spreadsheetName` option must be used. Similarly for worksheets (`raw.worksheets`), the `worksheetName` options must be used.
258 |
259 | #### Options
260 |
261 | ##### `callback`
262 |
263 | Function returning the authenticated Spreadsheet instance.
264 |
265 | ##### `debug`
266 |
267 | If `true`, will display colourful console logs outputing current actions.
268 |
269 | ##### `username` `password`
270 |
271 | Google account - _Be careful about committing these to public repos_.
272 |
273 | ##### `oauth`
274 |
275 | OAuth configuration object. See [google-oauth-jwt](https://github.com/extrabacon/google-oauth-jwt#specifying-options). _By default `oauth.scopes` is set to `['https://spreadsheets.google.com/feeds']` (`https` if `useHTTPS`)_
276 |
277 | ##### `accessToken`
278 |
279 | Reuse a generated access `token` of the given `type`. If you set `accessToken` to an object, reauthentications will not work. Instead use a `function accessToken(callback(err, token)) { ... }` function, to allow token generation when required.
280 |
281 | ##### `spreadsheetName` `spreadsheetId`
282 |
283 | The spreadsheet you wish to edit. Either the Name or Id is required.
284 |
285 | ##### `worksheetName` `worksheetId`
286 |
287 | The worksheet you wish to edit. Either the Name or Id is required.
288 |
289 | ##### `useHTTPS`
290 |
291 | Whether to use `https` when connecting to Google (default: `true`)
292 |
293 | ##### `useCellTextValues`
294 |
295 | Return text values for cells or return values as typed. (default: `true`)
296 |
297 | #### Todo
298 |
299 | - Create New Spreadsheets
300 | - Read specific range of cells
301 | - Option to cache auth token in file
302 |
303 | #### FAQ
304 |
305 | - Q: How do I append rows to my spreadsheet ?
306 | - A: Using the `info` object returned from `receive()`, one could always begin `add()`ing at the `lastRow + 1`, thereby appending to the spreadsheet.
307 |
308 | #### Credits
309 |
310 | Thanks to `googleclientlogin` for easy Google API ClientLogin Tokens
311 |
312 | #### References
313 |
314 | - https://developers.google.com/google-apps/spreadsheets/
315 |
316 | #### Donate
317 |
318 | BTC 1AxEWoz121JSC3rV8e9MkaN9GAc5Jxvs4
319 |
320 | #### MIT License
321 |
322 | Copyright © 2015 Jaime Pillora <dev@jpillora.com>
323 |
324 | Permission is hereby granted, free of charge, to any person obtaining
325 | a copy of this software and associated documentation files (the
326 | 'Software'), to deal in the Software without restriction, including
327 | without limitation the rights to use, copy, modify, merge, publish,
328 | distribute, sublicense, and/or sell copies of the Software, and to
329 | permit persons to whom the Software is furnished to do so, subject to
330 | the following conditions:
331 |
332 | The above copyright notice and this permission notice shall be
333 | included in all copies or substantial portions of the Software.
334 |
335 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
336 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
337 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
338 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
339 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
340 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
341 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
342 |
--------------------------------------------------------------------------------
/lib/spreadsheet.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | //module for using the google api to get anayltics data in an object
4 | require("colors");
5 | var request = require("request");
6 | var _ = require("lodash");
7 | var auth = require("./auth");
8 | var util = require("./util");
9 | var Metadata = require("./metadata");
10 | var async = require("async");
11 | var xml2js = require("xml2js");
12 |
13 | //public api
14 | exports.create = exports.load = util.promisify(function(opts, callback) {
15 | if (!callback) {
16 | callback = opts.callback;
17 | }
18 | if (
19 | !(opts.username && opts.password) &&
20 | !opts.oauth &&
21 | !opts.oauth2 &&
22 | !opts.accessToken
23 | ) {
24 | return callback("Missing authentication information");
25 | } else if (!opts.spreadsheetId && !opts.spreadsheetName) {
26 | return callback("Missing 'spreadsheetId' or 'spreadsheetName'");
27 | } else if (!opts.worksheetId && !opts.worksheetName) {
28 | return callback("Missing 'worksheetId' or 'worksheetName'");
29 | }
30 | //default to true if useCellTextValues is not set or defined.
31 | opts.useCellTextValues = _.has(opts, "useCellTextValues")
32 | ? opts.useCellTextValues
33 | : true;
34 |
35 | var spreadsheet = new Spreadsheet(opts);
36 |
37 | //default to http's' when undefined
38 | opts.useHTTPS = opts.useHTTPS === false ? "" : "s";
39 | spreadsheet.protocol += opts.useHTTPS;
40 |
41 | //add to spreadsheet
42 | _.extend(
43 | spreadsheet,
44 | _.pick(
45 | opts,
46 | "spreadsheetId",
47 | "spreadsheetName",
48 | "worksheetId",
49 | "worksheetName",
50 | "debug"
51 | )
52 | );
53 |
54 | spreadsheet.log("Logging into Google...".grey);
55 | auth(opts, function(err, token) {
56 | if (err) return callback(err);
57 | spreadsheet.log("Logged into Google".green);
58 | spreadsheet.setToken(token);
59 | spreadsheet.init(callback);
60 | });
61 | });
62 |
63 | //spreadsheet class
64 | function Spreadsheet(opts) {
65 | this.opts = opts;
66 | this.raw = {};
67 | this.protocol = "http";
68 | this.xmlParser = new xml2js.Parser({
69 | charkey: "$t",
70 | explicitArray: false,
71 | explicitCharkey: false,
72 | mergeAttrs: true,
73 | valueProcessors: [xml2js.processors.parseNumbers],
74 | attrValueProcessors: [xml2js.processors.parseNumbers]
75 | });
76 | this.reset();
77 | }
78 |
79 | Spreadsheet.prototype.init = function(callback) {
80 | var _this = this;
81 | this.getSheetId("spread", function(err) {
82 | if (err) return callback(err, null);
83 | _this.getSheetId("work", function(err) {
84 | if (err) return callback(err, null);
85 | _this.setTemplates();
86 | callback(null, _this);
87 | });
88 | });
89 | };
90 |
91 | Spreadsheet.prototype.log = function() {
92 | if (this.debug) console.log.apply(console, arguments);
93 | };
94 |
95 | //spreadsheet.request wraps mikeal's request with
96 | //google spreadsheet specific additions (adds token, follow)
97 | Spreadsheet.prototype.request = util.promisify(function(opts, callback) {
98 | if (!_.isPlainObject(opts) || !opts.url) {
99 | return callback("Invalid request");
100 | }
101 | if (!this.authHeaders) {
102 | return callback("No authorization token. Use auth() first.");
103 | }
104 | //use pre-generated authenication headers
105 | opts.headers = this.authHeaders;
106 |
107 | //default to GET
108 | if (!opts.method) opts.method = "GET";
109 |
110 | //follow redirects - even from POSTs
111 | opts.followAllRedirects = true;
112 |
113 | var _this = this;
114 | request(opts, function(err, response, body) {
115 | //show error
116 | if (err) return callback(err);
117 | //missing the response???
118 | if (!response) return callback("no response");
119 | //reauth
120 | if (
121 | response.statusCode === 401 &&
122 | typeof _this.opts.accessToken !== "object"
123 | ) {
124 | _this.log(
125 | "Authentication token expired. Logging into Google again...".grey
126 | );
127 | return auth(_this.opts, function(err, token) {
128 | if (err) return callback(err);
129 | _this.setToken(token);
130 | _this.request(opts, callback);
131 | });
132 | }
133 | //body is error
134 | if (response.statusCode !== 200) return callback(body);
135 | //try to parse XML
136 | _this.xmlParser.parseString(body, callback);
137 | });
138 | });
139 |
140 | //get spreadsheet/worksheet ids by name
141 | Spreadsheet.prototype.getSheetId = util.promisify(function(type, callback) {
142 | var _this = this;
143 | var id = type + "sheetId";
144 | var display = type.charAt(0).toUpperCase() + type.substr(1) + "sheet";
145 | var name = this[type + "sheetName"];
146 | var spreadsheetUrlId = type === "work" ? "/" + this.spreadsheetId : "";
147 | //already have id
148 | if (this[id]) {
149 | return callback(null);
150 | }
151 | this.log(("Searching for " + display + " '" + name + "'...").grey);
152 | this.request(
153 | {
154 | url:
155 | this.protocol +
156 | "://spreadsheets.google.com/feeds/" +
157 | type +
158 | "sheets" +
159 | spreadsheetUrlId +
160 | "/private/full"
161 | },
162 | function(err, result) {
163 | if (err) return callback(err);
164 | var entries = result.feed.entry || [];
165 | // Force array format for result
166 | if (!(entries instanceof Array)) {
167 | entries = [entries];
168 | }
169 | //store raw mapped results
170 | _this.raw[type + "sheets"] = entries.map(function(e1) {
171 | var e2 = {};
172 | for (var prop in e1) {
173 | var val = e1[prop];
174 | //remove silly $t object
175 | if (typeof val === "object") {
176 | var keys = Object.keys(val);
177 | if (keys.length === 1 && keys[0] === "$t") val = val.$t;
178 | }
179 | //remove silly gs$
180 | if (/^g[a-z]\$(\w+)/.test(prop)) {
181 | e2[RegExp.$1] = isNaN(Number(val)) ? val : Number(val);
182 | } else {
183 | e2[prop] = isNaN(Number(val)) ? val : Number(val);
184 | }
185 | }
186 | //search for 'name', extract only end portion of URL!
187 | if (e2.title === name && e2.id && /([^\/]+)$/.test(e2.id)) {
188 | _this[id] = RegExp.$1;
189 | }
190 | return e2;
191 | });
192 |
193 | var m = null;
194 | if (!_this[id]) return callback(type + "sheet '" + name + "' not found");
195 |
196 | _this.log(
197 | ("Tip: Use option '" +
198 | type +
199 | 'sheetId: "' +
200 | _this[id] +
201 | "\"' for improved performance").yellow
202 | );
203 | callback(null);
204 | }
205 | );
206 | });
207 |
208 | Spreadsheet.prototype.setToken = function(token) {
209 | this.authHeaders = {
210 | Authorization: token.type + " " + token.token,
211 | "Content-Type": "application/atom+xml",
212 | "GData-Version": "3.0",
213 | "If-Match": "*"
214 | };
215 | };
216 |
217 | Spreadsheet.prototype.baseUrl = function() {
218 | return (
219 | this.protocol +
220 | "://spreadsheets.google.com/feeds/cells/" +
221 | this.spreadsheetId +
222 | "/" +
223 | this.worksheetId +
224 | "/private/full"
225 | );
226 | };
227 |
228 | Spreadsheet.prototype.setTemplates = function() {
229 | this.bodyTemplate = _.template(
230 | '\n' +
233 | "" +
234 | this.baseUrl() +
235 | "\n" +
236 | "<%= entries %>\n" +
237 | "\n"
238 | );
239 |
240 | this.entryTemplate = _.template(
241 | "\n" +
242 | " UpdateR<%= row %>C<%= col %>\n" +
243 | ' \n' +
244 | " " +
245 | this.baseUrl() +
246 | "/R<%= row %>C<%= col %>\n" +
247 | ' \n' +
251 | ' \'/>\n' +
252 | "\n"
253 | );
254 | };
255 |
256 | Spreadsheet.prototype.reset = function() {
257 | //map { r: { c: CELLX, c: CELLY }}
258 | this.entries = {};
259 | //map { name: CELL }
260 | this.names = {};
261 | };
262 |
263 | Spreadsheet.prototype.add = function(cells) {
264 | //init data
265 | if (_.isArray(cells)) this.arr(cells, 0, 0);
266 | else this.obj(cells, 0, 0);
267 | };
268 |
269 | Spreadsheet.prototype.arr = function(arr, ro, co) {
270 | var i, j, rows, cols, rs, cs;
271 |
272 | // _this.log("Add Array: " + JSON.stringify(arr));
273 | ro = util.num(ro);
274 | co = util.num(co);
275 |
276 | rows = arr;
277 | for (i = 0, rs = rows.length; i < rs; ++i) {
278 | cols = rows[i];
279 | if (!_.isArray(cols)) {
280 | this.addVal(cols, i + 1 + ro, 1 + co);
281 | continue;
282 | }
283 | for (j = 0, cs = cols.length; j < cs; ++j) {
284 | this.addVal(cols[j], i + 1 + ro, j + 1 + co);
285 | }
286 | }
287 | return;
288 | };
289 |
290 | Spreadsheet.prototype.obj = function(obj, ro, co) {
291 | var row, col, cols;
292 |
293 | // _this.log("Add Object: " + JSON.stringify(obj));
294 | ro = util.num(ro);
295 | co = util.num(co);
296 |
297 | for (row in obj) {
298 | row = util.num(row);
299 | cols = obj[row];
300 |
301 | //insert array
302 | if (_.isArray(cols)) {
303 | this.arr(cols, row - 1, 0);
304 | continue;
305 | }
306 |
307 | //insert obj
308 | for (col in cols) {
309 | col = util.num(col);
310 | var data = cols[col];
311 | if (_.isArray(data)) this.arr(data, row - 1 + ro, col - 1 + co);
312 | else this.addVal(data, row + ro, col + co);
313 | }
314 | }
315 | };
316 |
317 | //dereference named cells {{ myCell }}
318 | Spreadsheet.prototype.getNames = function(curr) {
319 | var _this = this;
320 | return curr.val
321 | .replace(/\{\{\s*([\-\w\s]*?)\s*\}\}/g, function(str, name) {
322 | var link = _this.names[name];
323 | if (!link) return _this.log(("WARNING: could not find: " + name).yellow);
324 | return util.int2cell(link.row, link.col);
325 | })
326 | .replace(/\{\{\s*([\-\d]+)\s*,\s*([\-\d]+)\s*\}\}/g, function(both, r, c) {
327 | return util.int2cell(curr.row + util.num(r), curr.col + util.num(c));
328 | });
329 | };
330 |
331 | Spreadsheet.prototype.addVal = function(val, row, col) {
332 | // _this.log(("Add Value at R"+row+"C"+col+": " + val).white);
333 |
334 | if (!this.entries[row]) this.entries[row] = {};
335 | if (this.entries[row][col])
336 | this.log(("WARNING: R" + row + "C" + col + " already exists").yellow);
337 |
338 | var obj = {
339 | row: row,
340 | col: col
341 | },
342 | t = typeof val;
343 |
344 | if (t === "string" || t === "number" || t === "boolean") obj.val = val;
345 | else obj = _.extend(obj, val);
346 |
347 | if (obj.name)
348 | if (this.names[obj.name]) throw "Name already exists: " + obj.name;
349 | else this.names[obj.name] = obj;
350 |
351 | if (obj.val === undefined && !obj.ref)
352 | this.log(("WARNING: Missing value in: " + JSON.stringify(obj)).yellow);
353 |
354 | this.entries[row][col] = obj;
355 | };
356 |
357 | //convert pending batch into a string
358 | Spreadsheet.prototype.compile = function() {
359 | var row,
360 | col,
361 | strs = [];
362 | this.maxRow = 0;
363 | this.maxCol = 0;
364 | for (row in this.entries)
365 | for (col in this.entries[row]) {
366 | var obj = this.entries[row][col];
367 | this.maxRow = Math.max(this.maxRow, row);
368 | this.maxCol = Math.max(this.maxCol, col);
369 | if (typeof obj.val === "string") obj.val = this.getNames(obj);
370 |
371 | if (obj.val === undefined) continue;
372 | else obj.val = _.escape(obj.val.toString());
373 |
374 | strs.push(this.entryTemplate(obj));
375 | }
376 |
377 | return this.bodyTemplate({
378 | entries: strs.join("\n")
379 | });
380 | };
381 |
382 | //send (bulk) changes
383 | Spreadsheet.prototype.send = util.promisify(function(options, callback) {
384 | if (typeof options === "function") {
385 | callback = options;
386 | options = {};
387 | } else if (!callback) {
388 | callback = function() {};
389 | }
390 | if (!this.bodyTemplate || !this.entryTemplate)
391 | return callback(
392 | "No templates have been created. Use setTemplates() first."
393 | );
394 |
395 | var _this = this,
396 | body = this.compile();
397 |
398 | //finally send all the entries
399 | _this.log("Sending updates...".grey);
400 | // _this.log(entries.white);
401 | async.series(
402 | [
403 | function autoSize(next) {
404 | if (!options.autoSize) return next();
405 | _this.log("Determining worksheet size...".grey);
406 | _this.metadata(function(err, metadata) {
407 | if (err) return next(err);
408 |
409 | //no resize needed
410 | if (
411 | metadata.rowCount >= _this.maxRow &&
412 | metadata.colCount >= _this.maxCol
413 | )
414 | return next(null);
415 |
416 | _this.log("Resizing worksheet...".grey);
417 | //resize with maximums
418 | metadata.rowCount = Math.max(metadata.rowCount, _this.maxRow);
419 | metadata.colCount = Math.max(metadata.colCount, _this.maxCol);
420 | _this.metadata(metadata, next);
421 | });
422 | },
423 | function send(next) {
424 | _this.request(
425 | {
426 | method: "POST",
427 | url: _this.baseUrl() + "/batch",
428 | body: body
429 | },
430 | function(err, result) {
431 | var status = null;
432 |
433 | if (err) return next(err);
434 |
435 | if (!result.feed)
436 | return next(
437 | "Google Spreadsheets API has changed please post an issue!"
438 | );
439 |
440 | if (!result.feed.entry) {
441 | _this.log("No updates in current send()".yellow);
442 | _this.reset();
443 | return next(null);
444 | }
445 |
446 | var entries = _.isArray(result.feed.entry)
447 | ? result.feed.entry
448 | : [result.feed.entry];
449 |
450 | // DEBUG: displays raw response from Google
451 | // console.log(entries);
452 |
453 | var errors = entries
454 | .map(function(e) {
455 | return {status: e["batch:status"]};
456 | })
457 | .filter(function(e) {
458 | return e.status && e.status.code !== 200;
459 | });
460 |
461 | if (errors.length > 0) {
462 | _this.log("Error updating spreadsheet:");
463 | _.each(errors, function(e, i) {
464 | _this.log(
465 | " #" +
466 | (i + 1) +
467 | " [" +
468 | e.status.code +
469 | "] " +
470 | e.status.reason
471 | );
472 | });
473 |
474 | //concat error messages
475 | var msg =
476 | "Error updating spreadsheet: " +
477 | errors
478 | .map(function(e, i) {
479 | // var msg = errors.length > 1 ? ("#"+(i+1)) : "";
480 | var msg = e.status.reason;
481 | //swap out no message or a the silly "wait a bit" message
482 | if (
483 | !msg ||
484 | /Please wait a bit and try reloading your spreadsheet/.test(
485 | msg
486 | )
487 | )
488 | msg =
489 | "Your update may not fit in the worksheet. See the 'autoSize' option.";
490 | return msg;
491 | })
492 | .join(" ");
493 |
494 | next(msg);
495 | return;
496 | }
497 |
498 | _this.log("Successfully Updated Spreadsheet".green);
499 | //data has been successfully sent, clear it
500 | _this.reset();
501 | next(null);
502 | return;
503 | }
504 | );
505 | }
506 | ],
507 | callback
508 | );
509 | });
510 |
511 | //Get entire spreadsheet
512 | Spreadsheet.prototype.receive = util.promisify(function(options, callback) {
513 | if (typeof options === "function") {
514 | callback = options;
515 | options = {};
516 | }
517 | var _this = this;
518 | // get whole spreadsheet
519 | this.request(
520 | {
521 | url: this.baseUrl()
522 | },
523 | function(err, result) {
524 | if (!result || !result.feed) {
525 | err = "Error Reading Spreadsheet";
526 | _this.log(
527 | err.red.underline +
528 | "\nData:\n" +
529 | JSON.stringify(_this.entries, null, 2)
530 | );
531 | callback(err, null);
532 | return;
533 | }
534 |
535 | var entries = result.feed.entry || [];
536 | // Force array format for result
537 | if (!(entries instanceof Array)) {
538 | entries = [entries];
539 | }
540 | var rows = {};
541 | var info = {
542 | spreadsheetId: _this.spreadsheetId,
543 | worksheetId: _this.worksheetId,
544 | worksheetTitle: result.feed.title.$t || result.feed.title || null,
545 | worksheetUpdated:
546 | new Date(result.feed.updated.$t || result.feed.updated) || null,
547 | authors: result.feed.author && result.feed.author.name,
548 | totalCells: entries.length,
549 | totalRows: 0,
550 | lastRow: 1,
551 | nextRow: 1
552 | };
553 | var maxRow = 0;
554 |
555 | _.each(entries, function(entry) {
556 | var cell = entry["gs:cell"],
557 | r = cell.row,
558 | c = cell.col;
559 | if (!rows[r]) {
560 | info.totalRows++;
561 | rows[r] = {};
562 | }
563 |
564 | rows[r][c] = util.gcell2cell(
565 | cell,
566 | options.getValues,
567 | _this.opts.useCellTextValues
568 | );
569 | info.lastRow = util.num(r);
570 | });
571 |
572 | if (entries.length) info.nextRow = info.lastRow + 1;
573 |
574 | _this.log(
575 | ("Retrieved " +
576 | entries.length +
577 | " cells and " +
578 | info.totalRows +
579 | " rows").green
580 | );
581 |
582 | callback(null, rows, info);
583 | }
584 | );
585 | });
586 |
587 | Spreadsheet.prototype.metadata = util.promisify(function(data, callback) {
588 | var meta = new Metadata(this);
589 | if (typeof data === "function") {
590 | callback = data;
591 | meta.get(callback);
592 | return;
593 | } else if (!callback) {
594 | callback = function() {};
595 | }
596 | meta.set(data, callback);
597 | });
598 |
--------------------------------------------------------------------------------