├── .gitignore ├── index.js ├── HISTORY.md ├── tests ├── auth.js ├── index.js ├── spreadsheets.js ├── cells.js ├── worksheets.js └── rows.js ├── lib ├── utils.js ├── auth.js ├── index.js ├── spreadsheet.js ├── rows.js ├── cells.js └── worksheet.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // This file is just added for convenience so this repository can be 2 | // directly checked out into a project's deps folder 3 | module.exports = require('./lib/index'); -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## History 2 | 3 | - v0.0.3 June 18th, 2013 4 | - Included bug fixes from [mikael](http://github.com/mikael) and [balupton](http://github.com/balupton) - thanks guys! 5 | - v0.0.2 April 27th, 2013 6 | - Added forEach row iterator. 7 | - v0.0.1 April 14th, 2013 8 | - First version, basic Spreadsheets, Worksheets and Rows interfaces functional. Need Cells support and to refactor Rows interface to be a bit cleaner and more obvious to use. 9 | -------------------------------------------------------------------------------- /tests/auth.js: -------------------------------------------------------------------------------- 1 | var gsheets = require('../index'), 2 | async = require('async'); 3 | 4 | var theSheet = null, theWorksheet = null; 5 | 6 | module.exports = { 7 | 8 | "valid login": function(test) { 9 | test.expect(1); 10 | 11 | gsheets.auth({ 12 | email: process.env.GSHEETS_USER, 13 | password: process.env.GSHEETS_PASS 14 | }, function(err) { 15 | test.ifError(err); 16 | test.done(); 17 | }); 18 | }, 19 | 20 | "reject invalid login": function(test) { 21 | test.expect(1); 22 | gsheets.auth({ 23 | email: 'invalid@email', 24 | password: 'invalidpassword' 25 | }, function(err) { 26 | test.ok(err, "Invalid login should produce error callback"); 27 | test.done(); 28 | }); 29 | } 30 | 31 | }; -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | exports.makeUrl = function(feed, key, worksheetKey) { 3 | var url = ''; 4 | if (feed==='spreadsheets') { 5 | url = 'https://spreadsheets.google.com/feeds/spreadsheets/private/full/' + key; 6 | } else if (feed === 'worksheets') { 7 | url = 'https://spreadsheets.google.com/feeds/worksheets/' + key + '/private/full'; 8 | } else if (feed === 'worksheet') { 9 | url = 'https://spreadsheets.google.com/feeds/worksheets/' + key + '/private/full/' + worksheetKey; 10 | } else if (feed === 'list') { 11 | url = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheetKey + '/private/full'; 12 | } else if (feed === 'cells') { 13 | url = 'https://spreadsheets.google.com/feeds/cells/' + key + '/' + worksheetKey + '/private/full'; 14 | } 15 | return url; 16 | }; -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setUp: function(callback) { 3 | if (!process.env.GSHEETS_USER || !process.env.GSHEETS_PASS) { 4 | console.log('ERROR: Must specify GSHEETS_USER, GSHEETS_PASS credentials for google account on which to test'); 5 | return setTimeout(function() { 6 | process.exit(-1); 7 | },50); 8 | } 9 | if (!process.env.GSHEETS_TEST_KEY) { 10 | console.log('ERROR: Must specify GSHEETS_TEST_KEY pointing to a copy of the test document found here http://goo.gl/SsM4j'); 11 | return setTimeout(function() { 12 | process.exit(-1); 13 | },50); 14 | } 15 | callback(); 16 | }, 17 | 18 | 19 | "auth": require('./auth'), 20 | 21 | "spreadsheets": require('./spreadsheets'), 22 | 23 | "worksheets": require('./worksheets'), 24 | 25 | "rows": require('./rows') 26 | 27 | //"cells": require('./cells') 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-sheets", 3 | "version": "0.0.7", 4 | "description": "Read and modify Google Spreadsheets", 5 | "homepage": "https://github.com/benjamind/google-sheets", 6 | "keywords": [ 7 | "google", 8 | "gdata", 9 | "spreadsheets", 10 | "sheets", 11 | "google spreadsheets" 12 | ], 13 | "author": "Ben Delarre (http://www.delarre.net)", 14 | "maintainers": [ 15 | "Ben Delarre (https://github.com/benjamind)" 16 | ], 17 | "contributors": [ 18 | "Ben Delarre (https://github.com/benjamind)" 19 | ], 20 | "bugs": { 21 | "url": "https://github.com/benjamind/google-sheets/issues" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "http://github.com/benjamind/google-sheets.git" 26 | }, 27 | "engines": { 28 | "node": ">=0.4" 29 | }, 30 | "dependencies": { 31 | "googleclientlogin": "*", 32 | "xml2js": "*", 33 | "xmlbuilder": "0.4.3", 34 | "request": "*", 35 | "underscore": "*" 36 | }, 37 | "devDependencies": { 38 | "async": "*", 39 | "nodeunit": "*" 40 | }, 41 | "scripts": { 42 | "test": "nodeunit ./tests/index.js" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/spreadsheets.js: -------------------------------------------------------------------------------- 1 | var gsheets = require('../index'), 2 | async = require('async'); 3 | 4 | var theSheet = null, theWorksheet = null; 5 | 6 | module.exports = { 7 | 8 | setUp: function(callback) { 9 | gsheets.auth({ 10 | email: process.env.GSHEETS_USER, 11 | password: process.env.GSHEETS_PASS 12 | }, function(err) { 13 | if (err) { 14 | throw err; 15 | } 16 | callback(); 17 | }); 18 | }, 19 | tearDown: function(callback) { 20 | // cleanup? 21 | gsheets.logout(); 22 | callback(); 23 | }, 24 | 25 | "list spreadsheets": function(test) { 26 | test.expect(2); 27 | gsheets.list(function(err, sheets) { 28 | test.ifError(err); 29 | test.ok(sheets.length > 0, 'Found ' + sheets.length + ' spreadsheets in account, should be at least 1'); 30 | test.done(); 31 | }); 32 | }, 33 | 34 | "get spreadsheets": function(test) { 35 | test.expect(2); 36 | gsheets.getSpreadsheet(process.env.GSHEETS_TEST_KEY, function(err, sheet) { 37 | test.ifError(err); 38 | test.ok(sheet, 'Found a spreadsheet'); 39 | test.done(); 40 | }); 41 | }, 42 | 43 | "get invalid spreadsheet": function(test) { 44 | test.expect(2); 45 | gsheets.getSpreadsheet('invalidsheetkey', function(err, sheet) { 46 | test.ok(err, 'Should throw an error for invalid sheet'); 47 | test.ok(!sheet, 'Sheet should be null or undefined'); 48 | test.done(); 49 | }); 50 | } 51 | 52 | }; -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | var GoogleClientLogin = require('googleclientlogin').GoogleClientLogin; 2 | 3 | exports.authorize = function(options, callback) { 4 | var googleAuth = new GoogleClientLogin({ 5 | email: options.email, 6 | password: options.password, 7 | service: 'spreadsheets', 8 | accountType: GoogleClientLogin.accountTypes.google 9 | }); 10 | googleAuth.on(GoogleClientLogin.events.login, function(){ 11 | // do things with google services 12 | callback(null, googleAuth.getAuthId()); 13 | }); 14 | googleAuth.on(GoogleClientLogin.events.error, function(e) { 15 | switch(e.message) { 16 | case GoogleClientLogin.errors.loginFailed: 17 | if (this.isCaptchaRequired()) { 18 | return callback('captcha required', { 19 | error: 'Process captcha then recall function with captcha and token parameters', 20 | captchaUrl: this.getCaptchaUrl(), 21 | captchaToken: this.getCaptchaToken() 22 | }); 23 | } 24 | break; 25 | case GoogleClientLogin.errors.tokenMissing: 26 | case GoogleClientLogin.errors.captchaMissing: 27 | return callback('captcha missing', {error: 'You must pass the both captcha token and the captcha'}); 28 | } 29 | callback('unkown error',{error: 'Unknown error in GoogleClientLogin.'}); 30 | }); 31 | var captcha = undefined; 32 | if (options.captcha) { 33 | captcha = {logincaptcha: options.captcha, logintoken: options.token}; 34 | } 35 | googleAuth.login(captcha); 36 | }; -------------------------------------------------------------------------------- /tests/cells.js: -------------------------------------------------------------------------------- 1 | /* This is purely experimental! */ 2 | 3 | var gsheets = require('../index'), 4 | async = require('async'); 5 | 6 | var theSheet = null, theWorksheet = null; 7 | 8 | module.exports = { 9 | 10 | setUp: function(callback) { 11 | gsheets.auth({ 12 | email: process.env.GSHEETS_USER, 13 | password: process.env.GSHEETS_PASS 14 | }, function(err) { 15 | if (err) { 16 | throw err; 17 | } 18 | gsheets.getSpreadsheet(process.env.GSHEETS_TEST_KEY, function(err, sheet) { 19 | if (err) { 20 | throw err; 21 | } 22 | theSheet = sheet; 23 | callback(); 24 | }); 25 | }); 26 | }, 27 | tearDown: function(callback) { 28 | theSheet = null; 29 | callback(); 30 | }, 31 | 32 | "get all cells": function(test) { 33 | test.expect(4); 34 | // get first worksheet and retrieve its rows 35 | theSheet.getWorksheet('Sheet1', function(err, worksheet) { 36 | test.ifError(err); 37 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 38 | // get rows 39 | worksheet.getCells({maxRow: worksheet.meta.rows}, function(err, cells) { 40 | test.ifError(err); 41 | test.ok(cells instanceof gsheets.Cells, 'Should return an instance of Cells'); 42 | var rowData = cells.getRows(); 43 | console.log(Object.keys(rowData)); 44 | //test.ok(rowData['6'].email.data == 'zumbino@gmail.com'); 45 | var testRow = cells.getRow(6); 46 | var rowToUpdate = {}; 47 | Object.keys(testRow).forEach(function(k) { 48 | rowToUpdate[k] = testRow[k].data; 49 | }); 50 | rowToUpdate.email = 'final value'; 51 | console.log(rowToUpdate); 52 | Object.keys(rowToUpdate).forEach(function(k) { 53 | if (testRow[k]) { 54 | testRow[k].data = rowToUpdate[k]; 55 | } 56 | }); 57 | cells.update(testRow, function(err) { 58 | test.done(); 59 | }); 60 | }); 61 | }); 62 | }, 63 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Sheets 2 | A simple to use library for interacting with Google Spreadsheets. 3 | 4 | ## Features (todo list) 5 | 6 | ### Spreadsheets 7 | - [x] List 8 | - [x] Get 9 | 10 | ### Worksheets 11 | - [x] List 12 | - [x] Get 13 | - [x] Add 14 | - [x] Remove 15 | - [x] Resize 16 | 17 | ### Rows 18 | - [x] List 19 | - [x] Orderby & reverse support 20 | - [x] Simple Query support 21 | - [x] Remove 22 | - [x] Modify 23 | 24 | ### Cells 25 | - [ ] Modify 26 | - [ ] Get 27 | 28 | ## How to use 29 | 30 | ```javascript 31 | var gsheets = require('google-sheets'); 32 | 33 | // authorize your account 34 | gsheets.auth({ 35 | email: , 36 | password: 37 | }, function(err) { 38 | if (err) { 39 | throw err; 40 | } 41 | 42 | // list spreadsheets in the account 43 | gsheets.list(function(err, sheets) { 44 | // sheets is an array of Spreadsheet objects 45 | }); 46 | 47 | // load a specific sheet 48 | gsheets.getSpreadsheet(, function(err, sheet) { 49 | if (err) { 50 | throw err; 51 | } 52 | 53 | // sheet is a Spreadsheet object....lets list all its worksheets 54 | sheet.getWorksheets(function(err, worksheets) { 55 | if (err) { 56 | throw err; 57 | } 58 | // loop over the worksheets and print their titles 59 | Array.forEach(worksheets, function(worksheet) { 60 | console.log('Worksheet : ' + worksheet.getTitle()); 61 | }); 62 | 63 | // set size of first worksheet 64 | worksheets[0].set({ 65 | rows: 50, 66 | cols: 50 67 | }); 68 | // save it 69 | worksheet[0].save(function(err, worksheet) { 70 | // worksheet now refers to the updated worksheet object 71 | // lets get its rows and add some new ones 72 | worksheet.getRows(function(err, rows) { 73 | rows.create({ 74 | id: 1, 75 | date: new Date().toUTCString(), 76 | value: 'A new value' 77 | }, function(err, row) { 78 | // now delete it again 79 | rows.remove(row, function(err) { 80 | // remove succeeded 81 | }); 82 | }); 83 | }); 84 | }); 85 | }); 86 | 87 | }); 88 | }); 89 | ``` 90 | 91 | ## Documentation 92 | 93 | ### Authorization 94 | Current Google Sheets only supports authorized usage via GoogleClientLogin. It also only supports accessing spreadsheets through the private urls with the full projection. If this doesn't make any sense go read the [Google Spreadsheets API documentation](https://developers.google.com/google-apps/spreadsheets/). 95 | 96 | ```javascript 97 | var gsheets = require('google-sheets'); 98 | 99 | // authorize your account 100 | gsheets.auth({ 101 | email: , 102 | password: 103 | }, function(err) { 104 | ``` 105 | 106 | ### Spreadsheets 107 | 108 | #### List 109 | 110 | #### Get 111 | 112 | ### Worksheets 113 | 114 | #### List 115 | #### Get 116 | #### Rename 117 | #### Resize 118 | 119 | ### Rows 120 | Rows support is operational, but is not yet stable. The interface is likely to change as I don't like the architecture currently, however it does all currently work. 121 | 122 | ### Get Rows 123 | ### Orderby 124 | ### Reverse 125 | ### Simple Query 126 | ### Modify Rows 127 | 128 | ### Cells 129 | Cells support is not currently implemented but is planned. 130 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var Rows = require('./rows').Rows, 2 | Cells = require('./cells').Cells, 3 | Worksheet = require('./worksheet').Worksheet, 4 | Spreadsheet = require('./spreadsheet').Spreadsheet, 5 | authorize = require('./auth').authorize, 6 | makeUrl = require('./utils').makeUrl, 7 | request = require('request'), 8 | xml2js = require('xml2js'), 9 | url = require('url'); 10 | 11 | module.exports = { 12 | authId: null, 13 | 14 | Worksheet: Worksheet, 15 | Spreadsheet: Spreadsheet, 16 | Rows: Rows, 17 | Cells: Cells, 18 | 19 | auth: function(params, callback) { 20 | authorize(params, function(err, value) { 21 | if (err) { 22 | return callback(err,value); 23 | } 24 | this.authId = value; 25 | callback(null, this.authId); 26 | }.bind(this)); 27 | }, 28 | logout: function() { 29 | this.authId = null; 30 | }, 31 | parseSheet: function(entry) { 32 | var sheet = { 33 | lastModified: entry.updated, 34 | url: entry.id, 35 | author: { 36 | name: entry.author.name, 37 | email: entry.author.email 38 | }, 39 | title: entry.title._ 40 | }; 41 | if (entry.link && entry.link[1] && entry.link[1].href && entry.link[1].rel=='alternate') { 42 | var keyUrl = entry.link[1].href, 43 | parsedUrl = url.parse(keyUrl,true); 44 | 45 | sheet.key = parsedUrl.query.key; 46 | } 47 | sheet.id = entry.id.substring(entry.id.lastIndexOf('/')+1); 48 | return new Spreadsheet(sheet, this.authId); 49 | }, 50 | 51 | list: function(callback) { 52 | var listUrl = 'https://spreadsheets.google.com/feeds/spreadsheets/private/full', that = this; 53 | 54 | // do request for list of spreadsheets 55 | if (!this.authId) { 56 | return callback('auth required'); 57 | } 58 | 59 | this.query({ 60 | url: listUrl 61 | }, function(err, result) { 62 | if (err) { 63 | return callback(err); 64 | } 65 | var sheets = []; 66 | for(var i=0; i < result.feed.entry.length; i++) { 67 | var entry = result.feed.entry[i], 68 | sheet = that.parseSheet(entry); 69 | sheets.push(sheet); 70 | } 71 | callback(null,sheets); 72 | }); 73 | }, 74 | 75 | query: function(options, callback) { 76 | request({ 77 | url: options.url, 78 | method: options.method || 'GET', 79 | body: options.body || null, 80 | headers: { 81 | 'Authorization': 'GoogleLogin auth=' + this.authId 82 | } 83 | }, function(error, response, body) { 84 | if (error) { 85 | return callback(error); 86 | } 87 | var parser = new xml2js.Parser({ 88 | explicitArray: false, 89 | async: true, 90 | mergeAttrs: true 91 | }); 92 | parser.parseString(body, function(err, result) { 93 | callback(err, result); 94 | }); 95 | }); 96 | }, 97 | 98 | getSpreadsheet: function(key, callback) { 99 | // creates a spreadsheet object, having first confirmed the sheet exists 100 | //try and get the url 101 | if (!this.authId) { 102 | return callback('auth required'); 103 | } 104 | var sheetUrl = makeUrl('spreadsheets',key), that = this; 105 | this.query({ 106 | url: sheetUrl 107 | },function(err, result) { 108 | if (err) { 109 | return callback(err); 110 | } 111 | if (!result) { 112 | return callback(null,null); 113 | } 114 | // assume it worked, parse the spreadsheet 115 | callback(null, that.parseSheet(result.entry)); 116 | }); 117 | } 118 | }; -------------------------------------------------------------------------------- /lib/spreadsheet.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | builder = require('xmlbuilder'), 3 | xml2js = require('xml2js'), 4 | Worksheet = require('./worksheet').Worksheet, 5 | makeUrl = require('./utils').makeUrl, 6 | _ = require('underscore'); 7 | 8 | function Spreadsheet(meta, authId) { 9 | this.meta = meta; 10 | this.authId = authId; 11 | this.worksheets = []; 12 | } 13 | Spreadsheet.prototype = { 14 | 15 | query: function(options, callback) { 16 | request({ 17 | url: options.url, 18 | method: options.method || 'GET', 19 | body: options.body || null, 20 | headers: { 21 | 'Authorization': 'GoogleLogin auth=' + this.authId, 22 | 'Content-Type': 'application/atom+xml' 23 | } 24 | }, function(error, response, body) { 25 | if (error) { 26 | return callback(error); 27 | } 28 | var parser = new xml2js.Parser({ 29 | explicitArray: false, 30 | async: true, 31 | mergeAttrs: true 32 | }); 33 | parser.parseString(body, function(err, result) { 34 | callback(err, result); 35 | }); 36 | }); 37 | }, 38 | 39 | getWorksheetAt: function(index, callback) { 40 | this.getWorksheets(function(err, worksheets) { 41 | if (err) { 42 | return callback(err); 43 | } 44 | if (index >= worksheets.length) { 45 | return callback('index out of range, only ' + worksheets.length + ' worksheets in this spreadsheet'); 46 | } 47 | callback(null, worksheets[index]); 48 | }); 49 | }, 50 | getWorksheet: function(name, callback) { 51 | this.getWorksheets(function(err, worksheets) { 52 | if (err) { 53 | return callback(err); 54 | } 55 | for (var i=0; i < worksheets.length; i++) { 56 | if (worksheets[i].getTitle()===name) { 57 | return callback(null, worksheets[i]); 58 | } 59 | } 60 | return callback('no worksheet with title ' + name); 61 | }); 62 | }, 63 | 64 | getWorksheets: function(callback) { 65 | var that = this; 66 | this.query({ 67 | url: makeUrl('worksheets',this.meta.id) 68 | }, function(err, result) { 69 | if (err) { 70 | return callback(err); 71 | } 72 | var worksheets = []; 73 | 74 | if ( Array.isArray(result.feed.entry) === false ) { 75 | result.feed.entry = [result.feed.entry]; 76 | } 77 | 78 | function addEntry (entry) { 79 | var worksheet = new Worksheet({spreadsheetId: that.meta.id},that.authId); 80 | worksheet.parseJSON(entry); 81 | worksheets.push(worksheet); 82 | } 83 | if (typeof result.feed.entry.length === 'number') { 84 | for(var i=0; i < result.feed.entry.length; i++) { 85 | addEntry(result.feed.entry[i]); 86 | } 87 | } else { 88 | addEntry(result.feed.entry); 89 | } 90 | 91 | that.worksheets = worksheets; 92 | callback(null, worksheets); 93 | }); 94 | }, 95 | deleteWorksheet: function(worksheet, callback) { 96 | if (!(worksheet instanceof Worksheet)) { 97 | callback('Not a worksheet instance'); 98 | } else { 99 | worksheet.remove(callback); 100 | } 101 | }, 102 | addWorksheet: function(data, callback) { 103 | var worsheet = null; 104 | 105 | if (data instanceof Worksheet) { 106 | // just add it to our array, and tell worksheet to save 107 | data.meta.spreadsheetId = this.meta.id; 108 | worksheet = data; 109 | } else if (typeof data === 'object') { 110 | data.spreadsheetId = this.meta.id; 111 | worksheet = new Worksheet(data, this.authId); 112 | } 113 | 114 | this.worksheets.push(worksheet); 115 | worksheet.save(callback); 116 | } 117 | }; 118 | 119 | exports.Spreadsheet = Spreadsheet; 120 | -------------------------------------------------------------------------------- /tests/worksheets.js: -------------------------------------------------------------------------------- 1 | var gsheets = require('../index'), 2 | async = require('async'); 3 | 4 | var theSheet = null, theWorksheet = null; 5 | 6 | module.exports = { 7 | 8 | setUp: function(callback) { 9 | gsheets.auth({ 10 | email: process.env.GSHEETS_USER, 11 | password: process.env.GSHEETS_PASS 12 | }, function(err) { 13 | if (err) { 14 | throw err; 15 | } 16 | gsheets.getSpreadsheet(process.env.GSHEETS_TEST_KEY, function(err, sheet) { 17 | if (err) { 18 | throw err; 19 | } 20 | theSheet = sheet; 21 | callback(); 22 | }); 23 | }); 24 | }, 25 | tearDown: function(callback) { 26 | theSheet = null; 27 | callback(); 28 | }, 29 | "get single worksheet": function(test) { 30 | test.expect(4); 31 | gsheets.getSpreadsheet('0Ak3gStO7i2cYdE0wdm1FNG1hOXh6V25aQl81bXBjTHc', function(err, sheet) { 32 | test.ifError(err); 33 | theSheet = sheet; 34 | theSheet.getWorksheets(function(err, worksheets) { 35 | test.ifError(err); 36 | test.ok(Array.isArray(worksheets), 'Should get an array of worksheets'); 37 | test.ok(worksheets.length === 1, 'Should get 1 worksheets from the sheet got ' + worksheets.length); 38 | test.done(); 39 | }); 40 | }); 41 | }, 42 | "get worksheets": function(test) { 43 | test.expect(3); 44 | theSheet.getWorksheets(function(err, worksheets) { 45 | test.ifError(err); 46 | test.ok(Array.isArray(worksheets), 'Should get an array of worksheets'); 47 | test.ok(worksheets.length === 3, 'Should get 3 worksheets from the sheet (check the test sheet is in its correct state)'); 48 | test.done(); 49 | }); 50 | }, 51 | "get worksheet by index": function(test) { 52 | test.expect(3); 53 | theSheet.getWorksheetAt(1,function(err, worksheet) { 54 | test.ifError(err); 55 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 56 | test.ok(worksheet.getTitle()=='2nd Sheet', "Got the wrong worksheet, expected '2nd Sheet' got " + worksheet.getTitle()); 57 | test.done(); 58 | }); 59 | }, 60 | "get worksheet by invalid index": function(test) { 61 | test.expect(1); 62 | theSheet.getWorksheetAt(4,function(err, worksheet) { 63 | test.ok(err, 'Should throw an out of bounds error'); 64 | test.done(); 65 | }); 66 | }, 67 | "get worksheet by name": function(test) { 68 | test.expect(3); 69 | theSheet.getWorksheet('& a third sheet!',function(err, worksheet) { 70 | test.ifError(err); 71 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 72 | test.ok(worksheet.getTitle()=='& a third sheet!', "Got the wrong worksheet, expected '& a third sheet!' got " + worksheet.getTitle()); 73 | test.done(); 74 | }); 75 | }, 76 | "get worksheet by invalid name": function(test) { 77 | test.expect(1); 78 | theSheet.getWorksheet('some invalid worksheet name',function(err, worksheet) { 79 | test.ok(err, 'Should throw a not found error'); 80 | test.done(); 81 | }); 82 | }, 83 | "no add worksheet permission": function(test) { 84 | test.expect(1); 85 | gsheets.getSpreadsheet('0Ak3gStO7i2cYdGRrRU9GWF83UW1kTHB4eW1SbTE3M3', function(err, sheet) { 86 | test.ok(err, "Should throw an error if you don't have permission"); 87 | test.done(); 88 | }); 89 | }, 90 | "add and delete worksheet": function(test) { 91 | test.expect(4); 92 | theSheet.addWorksheet({ 93 | title: 'A Test Worksheet' 94 | }, function(err, worksheet) { 95 | test.ifError(err); 96 | 97 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 98 | 99 | test.ok(worksheet.getTitle() === 'A Test Worksheet'); 100 | 101 | // now delete it if this fails, fail the test as we'll corrupt our data! 102 | theSheet.deleteWorksheet(worksheet, function(err) { 103 | test.ifError(err); 104 | test.done(); 105 | }); 106 | }); 107 | }, 108 | "save worksheet": function(test) { 109 | test.expect(7); 110 | theSheet.addWorksheet({ 111 | title: 'A Test Worksheet' 112 | }, function(err, worksheet) { 113 | test.ifError(err); 114 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 115 | test.ok(worksheet.meta.title === 'A Test Worksheet'); 116 | // rename it 117 | worksheet.set({ 118 | title: 'Another worksheet' 119 | }); 120 | worksheet.save(function(err, worksheet) { 121 | test.ifError(err); 122 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 123 | test.ok(worksheet.getTitle() === 'Another worksheet', 'Expected Another Worsheet got ' + worksheet.getTitle()); 124 | // now delete it if this fails, fail the test as we'll corrupt our data! 125 | theSheet.deleteWorksheet(worksheet, function(err) { 126 | test.ifError(err); 127 | test.done(); 128 | }); 129 | }); 130 | }); 131 | }, 132 | "resize worksheet": function(test) { 133 | test.expect(7); 134 | theSheet.addWorksheet({ 135 | title: 'A Test Worksheet', 136 | rows: 5 137 | }, function(err, worksheet) { 138 | test.ifError(err); 139 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 140 | test.ok(worksheet.meta.title === 'A Test Worksheet'); 141 | // rename it 142 | worksheet.set({ 143 | title: 'A new worksheet', 144 | rows: 34 145 | }); 146 | worksheet.save(function(err, worksheet) { 147 | test.ifError(err); 148 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 149 | test.ok(worksheet.meta.rows==34,'Should have 34 rows found ' + worksheet.meta.rows); 150 | theSheet.deleteWorksheet(worksheet, function(err) { 151 | test.ifError(err); 152 | test.done(); 153 | }); 154 | }); 155 | }); 156 | } 157 | }; -------------------------------------------------------------------------------- /lib/rows.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | builder = require('xmlbuilder'), 3 | xml2js = require('xml2js'), 4 | makeUrl = require('./utils').makeUrl, 5 | _ = require('underscore'); 6 | 7 | function Rows(data,authId) { 8 | this.authId = authId; 9 | this.rows = []; 10 | this.parse(data); 11 | } 12 | Rows.prototype = { 13 | query: function(options, callback) { 14 | request({ 15 | url: options.url, 16 | method: options.method || 'GET', 17 | body: options.body || null, 18 | headers: _.extend(options.headers || {} , { 19 | 'Authorization': 'GoogleLogin auth=' + this.authId, 20 | 'Content-Type': 'application/atom+xml' 21 | }) 22 | }, function(error, response, body) { 23 | if (error) { 24 | return callback(error); 25 | } 26 | var parser = new xml2js.Parser({ 27 | explicitArray: false, 28 | async: true, 29 | mergeAttrs: true 30 | }); 31 | if (body) { 32 | parser.parseString(body, function(err, result) { 33 | if (err) { 34 | return callback(body); 35 | } 36 | callback(err, result); 37 | }); 38 | } else { 39 | callback(); 40 | } 41 | }); 42 | }, 43 | parseEntry: function(entry) { 44 | if (!entry.id) { 45 | return null; 46 | } 47 | 48 | var row = { 49 | id: entry.id, 50 | lastModified: entry.updated, 51 | title: entry.title._, 52 | content: entry.content._, 53 | data: {} 54 | }; 55 | for (var k=0; k < entry.link.length; k++) { 56 | if (entry.link[k].rel == 'edit') { 57 | row.editUrl = entry.link[k].href; 58 | } 59 | } 60 | for (var key in entry) { 61 | if (key.indexOf('gsx:')===0) { 62 | var rowName = key.substr('gsx:'.length); 63 | if (!_.isEmpty(entry[key])) { 64 | row.data[rowName] = entry[key]; 65 | } 66 | } 67 | } 68 | return row; 69 | }, 70 | parse: function(data) { 71 | var i = 0; 72 | 73 | this.id = data.feed.id; 74 | 75 | // loop over links 76 | for (i=0; i < data.feed.link.length; i++) { 77 | if (data.feed.link[i].rel == 'http://schemas.google.com/g/2005#post') { 78 | this.postUrl = data.feed.link[i].href; 79 | } 80 | } 81 | this.totalResults = parseInt(data.feed['openSearch:totalResults'],10); 82 | this.startIndex = parseInt(data.feed['openSearch:startIndex'],10); 83 | if (data.feed['openSearch:itemsPerPage']) { 84 | this.itemsPerPage = parseInt(data.feed['openSearch:itemsPerPage'],10); 85 | } 86 | 87 | // loop over items and create Row objects 88 | var entries = data.feed.entry; 89 | var rows = [], row; 90 | if (Array.isArray(entries)) { 91 | for (i=0; i < entries.length; i++) { 92 | var entry = entries[i]; 93 | row = this.parseEntry(entry); 94 | rows.push(row); 95 | } 96 | } else if (entries) { 97 | row = this.parseEntry(entries); 98 | rows.push(row); 99 | } 100 | this.rows = rows; 101 | }, 102 | create: function(row, callback) { 103 | // modify a row, then pass it to this function to save it, only modifications to data info is allowed 104 | var doc = builder.create(), 105 | that = this; 106 | 107 | var document = doc.begin('entry') 108 | .att('xmlns','http://www.w3.org/2005/Atom') 109 | .att('xmlns:gsx','http://schemas.google.com/spreadsheets/2006/extended'); 110 | 111 | for (var key in row) { 112 | var keyname = key.toLowerCase().replace('[^a-z0-9]',''); 113 | document.ele('gsx:' + keyname) 114 | .txt(row[key]); 115 | } 116 | // post to edit url 117 | this.query({ 118 | url: this.postUrl, 119 | method: 'POST', 120 | body: doc.toString() 121 | }, function(err, data) { 122 | if (err) { 123 | return callback(err); 124 | } 125 | var row = that.parseEntry(data.entry); 126 | that.rows.push(row); 127 | callback(null, row); 128 | }); 129 | }, 130 | forEach: function(callback) { 131 | this.rows.forEach(callback); 132 | }, 133 | getRow: function(index) { 134 | return this.rows[index]; 135 | }, 136 | getRows: function() { 137 | return this.rows; 138 | }, 139 | remove: function(row, callback) { 140 | var that = this; 141 | this.query({ 142 | url: row.id, 143 | method: 'GET' 144 | }, function(err, rowData) { 145 | var entry = that.parseEntry(rowData.entry); 146 | that.query({ 147 | url: entry.editUrl, 148 | method: 'DELETE' 149 | }, function(err) { 150 | if (err) { 151 | return callback(err); 152 | } 153 | callback(); 154 | }); 155 | }); 156 | }, 157 | save: function(row, options, callback) { 158 | if (typeof options === 'function') { 159 | callback = options; 160 | options = {}; 161 | } 162 | // modify a row, then pass it to this function to save it, only modifications to data info is allowed 163 | if (!row.editUrl || !row.id) { 164 | return this.create(row, callback); 165 | } 166 | 167 | var doc = builder.create(), 168 | that = this; 169 | 170 | var document = doc.begin('entry') 171 | .att('xmlns','http://www.w3.org/2005/Atom') 172 | .att('xmlns:gsx','http://schemas.google.com/spreadsheets/2006/extended') 173 | .ele('id') 174 | .txt(row.id) 175 | .up(); 176 | for (var key in row.data) { 177 | if (row.data[key]) { 178 | document.ele('gsx:' + key) 179 | .txt(row.data[key]); 180 | } 181 | } 182 | // post to edit url 183 | this.query(_.extend(options, { 184 | url: row.editUrl, 185 | method: 'PUT', 186 | body: doc.toString() 187 | }), function(err, data) { 188 | if (err) { 189 | return callback(err); 190 | } 191 | var row = that.parseEntry(data.entry); 192 | if (!row) { 193 | return callback(new Error('Unable to save result - no id ' + (data.entry || '').toString().slice(0, 300))); 194 | } else { 195 | callback(null, row); 196 | } 197 | }); 198 | } 199 | }; 200 | 201 | exports.Rows = Rows; -------------------------------------------------------------------------------- /lib/cells.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | builder = require('xmlbuilder'), 3 | xml2js = require('xml2js'), 4 | makeUrl = require('./utils').makeUrl, 5 | _ = require('underscore'); 6 | 7 | function Cells(data,authId) { 8 | this.authId = authId; 9 | this.cells = []; 10 | this.colToHeaderMap = {}; 11 | this.headerToColMap = {}; 12 | this.rows = {}; 13 | this.parse(data); 14 | } 15 | Cells.prototype = { 16 | query: function(options, callback) { 17 | request({ 18 | url: options.url, 19 | method: options.method || 'GET', 20 | body: options.body || null, 21 | headers: _.extend(options.headers || {} , { 22 | 'Authorization': 'GoogleLogin auth=' + this.authId, 23 | 'Content-Type': 'application/atom+xml', 24 | 'If-Match': '*' 25 | }) 26 | }, function(error, response, body) { 27 | if (error) { 28 | return callback(error); 29 | } 30 | var parser = new xml2js.Parser({ 31 | explicitArray: false, 32 | async: true, 33 | mergeAttrs: true 34 | }); 35 | if (body) { 36 | parser.parseString(body, function(err, result) { 37 | if (err) { 38 | return callback(body); 39 | } 40 | callback(err, result); 41 | }); 42 | } else { 43 | callback(); 44 | } 45 | }); 46 | }, 47 | parseEntry: function(entry) { 48 | var cell = { 49 | id: entry.id, 50 | lastModified: entry.updated, 51 | title: entry.title._, 52 | content: entry.content._, 53 | data: {} 54 | }; 55 | for (var k=0; k < entry.link.length; k++) { 56 | if (entry.link[k].rel == 'edit') { 57 | cell.editUrl = entry.link[k].href; 58 | } 59 | } 60 | for (var key in entry) { 61 | if (key.indexOf('gs:cell')===0) { 62 | if (!_.isEmpty(entry[key])) { 63 | cell.data = entry[key]._; 64 | cell.row = entry[key].row; 65 | cell.col = entry[key].col; 66 | cell.inputValue = entry[key].inputValue; 67 | } 68 | } 69 | } 70 | return cell; 71 | }, 72 | parse: function(data) { 73 | var i = 0; 74 | 75 | this.id = data.feed.id; 76 | 77 | // loop over links 78 | for (i=0; i < data.feed.link.length; i++) { 79 | if (data.feed.link[i].rel == 'http://schemas.google.com/g/2005#post') { 80 | this.postUrl = data.feed.link[i].href; 81 | } 82 | } 83 | this.startIndex = parseInt(data.feed['openSearch:startIndex'],10); 84 | this.rowCount = parseInt(data.feed['gs:rowCount'], 10); 85 | this.colCount = parseInt(data.feed['gs:colCount'], 10); 86 | if (data.feed['openSearch:itemsPerPage']) { 87 | this.itemsPerPage = parseInt(data.feed['openSearch:itemsPerPage'],10); 88 | } 89 | 90 | // loop over items and create Row objects 91 | var entries = data.feed.entry; 92 | var cells = [], cell; 93 | if (Array.isArray(entries)) { 94 | for (i=0; i < entries.length; i++) { 95 | var entry = entries[i]; 96 | cell = this.parseEntry(entry); 97 | cells.push(cell); 98 | } 99 | } else if (entries) { 100 | cell = this.parseEntry(entries); 101 | cells.push(cell); 102 | } 103 | this.cells = cells; 104 | 105 | var self = this; 106 | this.cells.forEach(function(c) { 107 | if (c.row === '1') { 108 | var normalizedHeader = c.data.toString().replace(/\s+/g, '').toLowerCase(); 109 | self.colToHeaderMap[c.col] = normalizedHeader; 110 | self.headerToColMap[normalizedHeader] = c.col; 111 | } 112 | }); 113 | 114 | this.cells.forEach(function(c) { 115 | if (!self.rows[c.row]) { 116 | self.rows[c.row] = {}; 117 | } 118 | self.rows[c.row][self.colToHeaderMap[c.col]] = c; 119 | }); 120 | }, 121 | update: function(cells, callback) { 122 | if (!Array.isArray(cells)) { 123 | cells = Object.keys(cells).map(function(k) { 124 | return cells[k]; 125 | }); 126 | } 127 | // modify a row, then pass it to this function to save it, only modifications to data info is allowed 128 | var doc = builder.create(), 129 | that = this; 130 | 131 | var document = doc.begin('feed') 132 | .att('xmlns','http://www.w3.org/2005/Atom') 133 | .att('xmlns:batch','http://schemas.google.com/gdata/batch') 134 | .att('xmlns:gs','http://schemas.google.com/spreadsheets/2006'); 135 | document.ele('id').txt(this.id).up(); 136 | 137 | cells.forEach(function(c) { 138 | var batchId = 'R' + c.row + 'C' + c.col; 139 | var entry = document.ele('entry'); 140 | if (c.title) { 141 | entry.ele('batch:id').txt(c.title).up(); 142 | } else { 143 | var title = that.numbersToLetters(parseInt(c.col, 10)) + c.row; 144 | entry.ele('batch:id').txt(title).up(); 145 | } 146 | entry.ele('batch:operation').att('type', 'update'); 147 | if (c.id) { 148 | entry.ele('id').txt(c.id).up(); 149 | } else { 150 | entry.ele('id').txt(that.id + '/' + batchId).up(); 151 | } 152 | var editLink = entry.ele('link') 153 | .att('rel', 'edit') 154 | .att('type', 'application/atom+xml'); 155 | if (c.editUrl) { 156 | editLink.att('href', c.editUrl); 157 | } else { 158 | editLink.att('href', that.id + '/' + batchId + '/version'); 159 | } 160 | 161 | entry.ele('gs:cell') 162 | .att('row', c.row.toString()) 163 | .att('col', c.col.toString()) 164 | .att('inputValue', c.data); 165 | }); 166 | // post to edit url 167 | var url = this.id + '/batch'; 168 | this.query({ 169 | url: url, 170 | method: 'POST', 171 | body: doc.toString() 172 | }, function(err, data) { 173 | if (err) { 174 | return callback(err); 175 | } 176 | return callback(null); 177 | // todo - really should update state based on result. 178 | /* 179 | var row = that.parseEntry(data.entry); 180 | that.rows.push(row); 181 | callback(null, row); 182 | */ 183 | }); 184 | }, 185 | forEach: function(callback) { 186 | this.rows.forEach(callback); 187 | }, 188 | getCell: function(row, col) { 189 | row = row.toString(); 190 | if (this.rows[row]) { 191 | return this.rows[row][this.colToHeaderMap[col.toString()]]; 192 | } else { 193 | return null; 194 | } 195 | }, 196 | getRow: function(row) { 197 | if (this.rows[row.toString()]) { 198 | return this.rows[row.toString()]; 199 | } else { 200 | return null; 201 | } 202 | }, 203 | getRows: function() { 204 | return this.rows; 205 | }, 206 | getCells: function() { 207 | return this.cells; 208 | }, 209 | numbersToLetters: function(num) { 210 | var base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', i, j = num, result = ''; 211 | while (j > 0) { 212 | var index = j % base.length; 213 | result = result + base[index - 1]; 214 | j = j - index; 215 | } 216 | return result; 217 | } 218 | }; 219 | 220 | exports.Cells = Cells; -------------------------------------------------------------------------------- /tests/rows.js: -------------------------------------------------------------------------------- 1 | var gsheets = require('../index'), 2 | async = require('async'); 3 | 4 | var theSheet = null, theWorksheet = null; 5 | 6 | module.exports = { 7 | 8 | setUp: function(callback) { 9 | gsheets.auth({ 10 | email: process.env.GSHEETS_USER, 11 | password: process.env.GSHEETS_PASS 12 | }, function(err) { 13 | if (err) { 14 | throw err; 15 | } 16 | gsheets.getSpreadsheet(process.env.GSHEETS_TEST_KEY, function(err, sheet) { 17 | if (err) { 18 | throw err; 19 | } 20 | theSheet = sheet; 21 | callback(); 22 | }); 23 | }); 24 | }, 25 | tearDown: function(callback) { 26 | theSheet = null; 27 | callback(); 28 | }, 29 | 30 | "get all rows": function(test) { 31 | test.expect(8); 32 | // get first worksheet and retrieve its rows 33 | theSheet.getWorksheet('First Sheet', function(err, worksheet) { 34 | test.ifError(err); 35 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 36 | // get rows 37 | worksheet.getRows(function(err, rows) { 38 | test.ifError(err); 39 | test.ok(rows instanceof gsheets.Rows, 'Should return an instance of Rows'); 40 | var rowData = rows.getRows(); 41 | test.ok(rowData.length==5, 'Should return 5 rows, got ' + rowData.length); 42 | test.ok(rowData[0].data.columna == '1', 'First row Column A should equal 1 got ' + rowData[0].data.columna); 43 | test.ok(rowData[4].data.columna == '5', '5th row Column A should equal 5 got ' + rowData[4].data.columna); 44 | test.ok(rowData[2].data.moremore == '\'this\' should "work"', '3rd row More & More should equal \'this\' should "work" got ' + rowData[2].data.moremore); 45 | test.done(); 46 | }); 47 | }); 48 | }, 49 | "get rows reversed orderedby": function(test) { 50 | test.expect(8); 51 | // get first worksheet and retrieve its rows 52 | theSheet.getWorksheet('First Sheet', function(err, worksheet) { 53 | test.ifError(err); 54 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 55 | // get rows 56 | worksheet.getRows({reverse: true, orderby: 'columna'}, function(err, rows) { 57 | test.ifError(err); 58 | test.ok(rows instanceof gsheets.Rows, 'Should return an instance of Rows'); 59 | var rowData = rows.getRows(); 60 | test.ok(rowData.length==5, 'Should return 5 rows, got ' + rowData.length); 61 | test.ok(rowData[4].data.columna == '1', 'First row Column A should equal 1 got ' + rowData[4].data.columna); 62 | test.ok(rowData[0].data.columna == '5', '5th row Column A should equal 5 got ' + rowData[0].data.columna); 63 | test.ok(rowData[2].data.moremore == '\'this\' should "work"', '3rd row More & More should equal \'this\' should "work" got ' + rowData[2].data.moremore); 64 | test.done(); 65 | }); 66 | }); 67 | }, 68 | "get rows simple query": function(test) { 69 | test.expect(6); 70 | // get first worksheet and retrieve its rows 71 | theSheet.getWorksheet('First Sheet', function(err, worksheet) { 72 | test.ifError(err); 73 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 74 | // get rows 75 | worksheet.getRows({sq: 'columna > 4'}, function(err, rows) { 76 | test.ifError(err); 77 | test.ok(rows instanceof gsheets.Rows, 'Should return an instance of Rows'); 78 | var rowData = rows.getRows(); 79 | test.ok(rowData.length==1, 'Should return 1 row only, got ' + rowData.length); 80 | test.ok(rowData[0].data.columna == '5', 'First row Column A should equal 5 got ' + rowData[0].data.columna); 81 | test.done(); 82 | }); 83 | }); 84 | }, 85 | 86 | 87 | "add and delete row": function(test) { 88 | test.expect(8); 89 | // get first worksheet and retrieve its rows 90 | theSheet.getWorksheet('2nd Sheet', function(err, worksheet) { 91 | test.ifError(err); 92 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 93 | worksheet.getRows(function(err, rows) { 94 | test.ifError(err); 95 | test.ok(rows instanceof gsheets.Rows, 'Should return an instance of Rows'); 96 | var i = 0; 97 | rows.create({ 98 | id: i, 99 | date: new Date().toUTCString(), 100 | value: 'A new value - ' + i 101 | }, function(err, row) { 102 | test.ifError(err); 103 | test.ok(row, 'Should return a row object'); 104 | // now delete it again 105 | rows.remove(row, function(err) { 106 | test.ifError(err); 107 | // update sheet size back to 20 so we have space for deletion 108 | worksheet.set({ 109 | rows: 20 110 | }); 111 | worksheet.save(function(err, worksheet) { 112 | test.ifError(err); 113 | test.done(); 114 | }); 115 | }); 116 | }); 117 | }); 118 | }); 119 | }, 120 | 121 | "add delete many rows": function(test) { 122 | test.expect(29); 123 | // get first worksheet and retrieve its rows 124 | theSheet.getWorksheet('2nd Sheet', function(err, worksheet) { 125 | test.ifError(err); 126 | test.ok(worksheet instanceof gsheets.Worksheet, 'Should return an instance of a worksheet'); 127 | worksheet.getRows(function(err, rows) { 128 | test.ifError(err); 129 | test.ok(rows instanceof gsheets.Rows, 'Should return an instance of Rows'); 130 | var i = 0; 131 | async.whilst( 132 | function() { 133 | return i < 10; 134 | }, 135 | function(callback) { 136 | // create a row 137 | rows.create({ 138 | id: i, 139 | date: new Date().toUTCString(), 140 | value: 'A new value - ' + i 141 | }, function(err, row) { 142 | i++; 143 | test.ifError(err); 144 | test.ok(row, 'Should return a row object'); 145 | callback(err); 146 | }); 147 | }, 148 | function(err) { 149 | test.ifError(err); 150 | test.ok(i==10, 'Should have created 10 rows'); 151 | worksheet.getRows(function(err, rows) { 152 | test.ifError(err); 153 | var rowData = rows.getRows(); 154 | test.ok(rowData.length==10, 'Should return 10 rows'); 155 | // resize worksheet to 0,0 to clear all data 156 | i = 0; 157 | async.each(rowData, function(item, callback) { 158 | rows.remove(item, callback); 159 | }, function(err) { 160 | // update sheet size back to 20 so we have space for deletion 161 | worksheet.set({ 162 | rows: 20 163 | }); 164 | worksheet.save(function(err, worksheet) { 165 | test.ifError(err); 166 | test.done(); 167 | }); 168 | }); 169 | }); 170 | } 171 | ); 172 | }); 173 | }); 174 | } 175 | }; -------------------------------------------------------------------------------- /lib/worksheet.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | builder = require('xmlbuilder'), 3 | xml2js = require('xml2js'), 4 | querystring = require('querystring'), 5 | Rows = require('./rows').Rows, 6 | Cells = require('./cells').Cells, 7 | makeUrl = require('./utils').makeUrl, 8 | _ = require('underscore'); 9 | 10 | function Worksheet(meta, authId) { 11 | this.meta = meta; 12 | this.authId = authId; 13 | } 14 | Worksheet.prototype = { 15 | createXML: function() { 16 | var doc = builder.create(), 17 | options = { 18 | title: this.meta.title || 'Untitled Worksheet', 19 | rows: this.meta.rows || 50, 20 | cols: this.meta.cols || 10 21 | }; 22 | 23 | doc.begin('entry') 24 | .att('xmlns','http://www.w3.org/2005/Atom') 25 | .att('xmlns:gs','http://schemas.google.com/spreadsheets/2006') 26 | .ele('title') 27 | .txt(options.title) 28 | .up() 29 | .ele('gs:rowCount') 30 | .txt(""+options.rows) 31 | .up() 32 | .ele('gs:colCount') 33 | .txt(""+options.cols); 34 | return doc.toString(); 35 | }, 36 | saveXML: function() { 37 | var doc = builder.create(); 38 | 39 | doc.begin('entry') 40 | .att('xmlns','http://www.w3.org/2005/Atom') 41 | .att('xmlns:gs','http://schemas.google.com/spreadsheets/2006') 42 | .ele('id') 43 | .txt(this.meta.url) 44 | .up() 45 | .ele('title') 46 | .att('type', 'text') 47 | .txt(this.meta.title) 48 | .up() 49 | .ele('gs:rowCount') 50 | .txt(""+this.meta.rows) 51 | .up() 52 | .ele('gs:colCount') 53 | .txt(""+this.meta.cols); 54 | return doc.toString(); 55 | }, 56 | create: function(callback) { 57 | var that = this; 58 | 59 | this.query({ 60 | url: makeUrl('worksheets',this.meta.spreadsheetId), 61 | method: 'POST', 62 | body: this.createXML() 63 | }, function(err, data) { 64 | if (err) { 65 | return callback(err); 66 | } 67 | that.parseJSON(data.entry); 68 | callback(null, that); 69 | }); 70 | 71 | }, 72 | save: function(callback) { 73 | if (!this.meta.editUrl) { 74 | // create instead 75 | return this.create(callback); 76 | } 77 | // otherwise save to edit url 78 | 79 | var that = this, 80 | xml = this.saveXML(); 81 | 82 | this.query({ 83 | url: this.meta.url, 84 | method: 'GET' 85 | }, function(err, entryData) { 86 | var entry = that.parseJSON(entryData.entry); 87 | that.query({ 88 | url: entry.editUrl, 89 | method: 'PUT', 90 | body: xml 91 | }, function(err, data) { 92 | if (err) { 93 | return callback(err); 94 | } 95 | that.parseJSON(data.entry); 96 | callback(null, that); 97 | }); 98 | }); 99 | }, 100 | remove: function(callback) { 101 | this.query({ 102 | url: this.meta.editUrl, 103 | method: 'DELETE' 104 | }, function(err) { 105 | callback(err); 106 | }); 107 | }, 108 | set: function(options) { 109 | this.meta = _.extend(this.meta, options); 110 | }, 111 | setTitle: function(title) { 112 | this.meta.title = title; 113 | }, 114 | getTitle: function() { 115 | return this.meta.title; 116 | }, 117 | query: function(options, callback) { 118 | request({ 119 | url: options.url, 120 | method: options.method || 'GET', 121 | body: options.body || null, 122 | headers: { 123 | 'Authorization': 'GoogleLogin auth=' + this.authId, 124 | 'Content-Type': 'application/atom+xml' 125 | } 126 | }, function(error, response, body) { 127 | if (error) { 128 | return callback(error); 129 | } 130 | if (body!==undefined) { 131 | var parser = new xml2js.Parser({ 132 | explicitArray: false, 133 | async: true, 134 | mergeAttrs: true 135 | }); 136 | parser.parseString(body, function(err, result) { 137 | if (err) { 138 | // if an error occurred in the parsing, we can assume we got a non-xml result from google 139 | // pass that error back instead 140 | return callback(body); 141 | } 142 | callback(err, result); 143 | }); 144 | } else { 145 | callback(); 146 | } 147 | }); 148 | }, 149 | parseJSON: function(entry) { 150 | var worksheet = { 151 | lastModified: entry.updated, 152 | title: entry.title._, 153 | url: entry.id, 154 | rows: parseInt(entry['gs:rowCount'],10), 155 | cols: parseInt(entry['gs:colCount'],10) 156 | }; 157 | // parse id from url 158 | worksheet.id = worksheet.url.substr(worksheet.url.lastIndexOf('/')+1); 159 | // loop over links 160 | for (var i=0; i < entry.link.length; i++) { 161 | if (entry.link[i].rel==='edit') { 162 | worksheet.editUrl = entry.link[i].href; 163 | } 164 | } 165 | this.meta = _.extend(this.meta, worksheet); 166 | return this.meta; 167 | }, 168 | 169 | getRows: function(options,callback) { 170 | if (typeof options === 'function') { 171 | callback = options; 172 | options = {}; 173 | } 174 | 175 | if (options.orderby) { 176 | options.orderby = 'gsx:' + options.orderby; 177 | } 178 | var sq = null; 179 | if (options.sq) { 180 | sq = options.sq; 181 | delete options.sq; 182 | } 183 | 184 | var that = this, 185 | url = makeUrl('list', this.meta.spreadsheetId, this.meta.id) + '?' + querystring.stringify(options); 186 | 187 | if (sq) { 188 | sq = encodeURIComponent(sq); 189 | sq = sq.replace('%3E', '>').replace('%3C','<'); 190 | url += '&sq=' + sq; 191 | } 192 | 193 | this.query({ 194 | url:url, 195 | method: 'GET' 196 | }, function(err, data) { 197 | if (err) { 198 | return callback(err); 199 | } 200 | 201 | // parse rows into row objects we can update and save in the worksheet 202 | var rows = new Rows(data, that.authId); 203 | that.rows = rows; 204 | callback(null, rows); 205 | }); 206 | }, 207 | 208 | getCells: function(options,callback) { 209 | if (typeof options === 'function') { 210 | callback = options; 211 | options = {}; 212 | } 213 | 214 | if (options.minRow) { 215 | options['min-row'] = options.minRow; 216 | delete options.minRow; 217 | } 218 | if (options.maxRow) { 219 | options['max-row'] = options.maxRow; 220 | delete options.maxRow; 221 | } 222 | 223 | if (options.minCol) { 224 | options['min-col'] = options.minCol; 225 | delete options.minCol; 226 | } 227 | if (options.maxCol) { 228 | options['max-col'] = options.maxCol; 229 | delete options.maxCol; 230 | } 231 | 232 | var that = this, 233 | url = makeUrl('cells', this.meta.spreadsheetId, this.meta.id) + '?' + querystring.stringify(options); 234 | 235 | this.query({ 236 | url:url, 237 | method: 'GET' 238 | }, function(err, data) { 239 | if (err) { 240 | return callback(err); 241 | } 242 | 243 | // parse rows into row objects we can update and save in the worksheet 244 | var cells = new Cells(data, that.authId); 245 | that.cells = cells; 246 | callback(null, cells); 247 | }); 248 | } 249 | }; 250 | 251 | exports.Worksheet = Worksheet; --------------------------------------------------------------------------------