grille
Version:
Simple CMS using Google Spreadsheets
229 lines (175 loc) • 5.81 kB
JavaScript
;
var async = require('async');
var _ = require('lodash');
var Worksheet = require('./worksheet.js');
var TimeoutError = require('./errors/timeout.js');
var DEFAULT_TIMEOUT = 5 * 1000;
var DEFAULT_PARALLEL_LIMIT = 5;
var DEFAULT_RETRY = 3;
/**
* Represents a collection of Worksheets within a Google Spreadsheet.
* Contains functionality for reading meta information and loading different tabs.
*
* @param String sheet_id The hash Google uses for representing this worksheet
* @param Number timeout How many ms should we wait before declaring Google's API dead
* @param Number parallel How many HTTP requests should we make in parrallel to the Google API
* @param Number retry How many times should we retry downloading a Worksheet from the Google API
*/
var Spreadsheet = function(sheet_id, timeout, parallel, retry) {
this.id = sheet_id;
this.timeout = timeout || DEFAULT_TIMEOUT;
this.parallel = parallel || DEFAULT_PARALLEL_LIMIT;
this.retry = retry || DEFAULT_RETRY;
this.meta_worksheet = new Worksheet(sheet_id, Spreadsheet.META);
this.meta = null;
this.content = {};
this.ready = false;
this.last_updated = null;
};
Spreadsheet.META = 'meta';
Spreadsheet.prototype.setTimeout = function(timeout) {
this.timeout = timeout;
};
Spreadsheet.prototype.load = function(load_callback) {
var self = this;
async.series({
meta: function(meta_callback) {
self.meta_worksheet.load(function(err, data) {
if (err) {
return meta_callback(err);
}
self.meta = data;
self.last_updated = self.meta_worksheet.last_updated;
meta_callback();
});
},
sheets: function(sheets_callback) {
var tabs = Object.keys(self.meta);
async.eachLimit(tabs, self.parallel, function(tab, cb) {
self.loadWorksheet(tab, self.meta[tab].format, cb);
}, function(err) {
if (err) {
return sheets_callback(err);
}
self.ready = true;
sheets_callback();
});
}
}, function(err) {
if (err) {
return load_callback(err);
}
self.ready = true;
load_callback(null, self.content);
});
};
Spreadsheet.prototype.toJSON = function() {
return this.content;
};
Spreadsheet.prototype.loadWorksheet = function(collection, type, callback, attempt) {
var self = this;
var callbackFired = false;
if (!attempt) {
attempt = 1;
}
var timeout = setTimeout(function() {
callbackFired = true;
if (attempt > self.retry) {
return callback(new TimeoutError(collection));
}
console.log("Re-Attempting to download worksheet:", collection, "attempt #:", attempt);
self.loadWorksheet(collection, type, callback, attempt + 1);
}, this.timeout);
var worksheet = new Worksheet(this.id, collection);
worksheet.load(function(err, data) {
if (callbackFired) {
// We got the data back from Google but it was too late.
return;
}
callbackFired = true;
clearTimeout(timeout);
if (err) {
return callback(err);
}
var path = self.meta[collection].collection;
if (type === 'hash') {
Spreadsheet.dotSet(self.content, data, path);
} else if (type === 'keyvalue') {
var new_data = Spreadsheet.extractKeyValue(data);
if (!self.content[path]) {
self.content[path] = {};
}
Spreadsheet.dotMerge(self.content, new_data, path);
} else if (type === 'array') {
Spreadsheet.dotSet(self.content, Spreadsheet.extractArray(data), path);
}
callback(null);
});
};
/**
* Takes a hash(row) of data containing setting the value column as the value for the whole hash
*
* {'a': {'value': 1}} => {'a': 1}
*/
Spreadsheet.extractKeyValue = function(keyvalues) {
Object.keys(keyvalues).map(function(key) {
keyvalues[key] = keyvalues[key].value;
});
return keyvalues;
};
/**
* Takes a hash(row) of data containing a bunch of col-X values and converts it into a 2D array
*
* {'1': {'id': 1, 'col-1': 2, 'col-2': 3}, '2': {'id': 2, 'col-1': 4, 'col-2': 5}} => [[2, 3], [4, 5]]
*/
Spreadsheet.extractArray = function(values) {
var array = [];
var rows = Object.keys(values).length;
var columns = Object.keys(values['1']).length - 1; // id column is 1
for (var y = 0; y < rows; y++) {
array[y] = [];
for (var x = 0; x < columns; x++) {
array[y][x] = values[y+1]['col-'+(x+1)];
}
}
return array;
};
/**
* Sets data in a deep object using dot notation
*
* @param destination Object Tree where we want to put the source
* @param source Mixed What we want to stick in the destination object
* @param path String The location in the tree to put the source, such that x.y.z => destination.x.y.z = source
*/
Spreadsheet.dotSet = function(destination, source, path) {
var nodes = path.split("."); // e.g. x, y, z
var pointer = destination;
for (var i = 0; i < nodes.length - 1; i++) {
if (!pointer[nodes[i]]) {
pointer[nodes[i]] = {};
}
pointer = pointer[nodes[i]];
}
pointer[nodes[nodes.length-1]] = source;
return destination;
};
/**
* Merges data in a deep object using dot notation
*
* @param destination Object Tree where we want to merge the source
* @param source Mixed What we want to stick in the destination object
* @param path String The location in the tree to put the source, such that x.y.z => destination.x.y.z = source
*/
Spreadsheet.dotMerge = function(destination, source, path) {
var nodes = path.split("."); // e.g. x, y, z
var pointer = destination;
for (var i = 0; i < nodes.length; i++) {
if (!pointer[nodes[i]]) {
pointer[nodes[i]] = {};
}
pointer = pointer[nodes[i]];
}
_.extend(pointer, source);
return destination;
};
module.exports = Spreadsheet;