├── .gitignore ├── Makefile ├── test ├── index.js ├── data.js ├── order.js ├── update.js ├── load.js ├── db.json ├── limit.js ├── error.json └── find.js ├── package.json ├── LICENSE ├── README.md ├── documentation.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | ./node_modules/.bin/mocha --reporter spec 3 | 4 | .PHONY: test -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing 3 | * This is the generic file for calling all of the tests. 4 | * It makes sure that everything is tested and explains each test. 5 | */ 6 | 7 | 8 | 9 | /** 10 | * .load(callback) 11 | * Makes sure that the data can be loaded 12 | */ 13 | require("./load"); 14 | 15 | /** 16 | * data.update(id) 17 | * Updates the local data with the external spreadsheet 18 | */ 19 | require("./update"); 20 | 21 | /** 22 | * .find(filter) 23 | * Attempts to filter the database with different parameters 24 | */ 25 | require("./find"); 26 | 27 | /** 28 | * data.order(field, desc) 29 | * Orders the data based on one field 30 | */ 31 | require("./order"); 32 | 33 | /** 34 | * data.limit(begin, end) 35 | * Limits the data retrieved 36 | */ 37 | require("./limit"); -------------------------------------------------------------------------------- /test/data.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { id: "1", firstname: "John" , lastname: "Smith" , age: "34", city: "New York", country: "USA" , timestamp: "12/10/2010" }, 3 | { id: "2", firstname: "Mery" , lastname: "Johnson" , age: "19", city: "Tokyo" , country: "Japan" , timestamp: "01/05/2003" }, 4 | { id: "3", firstname: "Peter" , lastname: "Williams", age: "45", city: "London" , country: "UK" , timestamp: "30/01/2007" }, 5 | { id: "4", firstname: "Laura" , lastname: "Brown" , age: "23", city: "Madrid" , country: "Spain" , timestamp: "16/05/2008" }, 6 | { id: "5", firstname: "Jack" , lastname: "Jones" , age: "56", city: "Paris" , country: "France", timestamp: "07/03/2009" }, 7 | { id: "6", firstname: "Martha", lastname: "Miller" , age: "73", city: "Rome" , country: "Italy" , timestamp: "27/11/2005" } 8 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drive-db", 3 | "version": "1.6.0", 4 | "description": "A simple google drive spreadsheet database", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/FranciscoP/drive-db.git" 12 | }, 13 | "keywords": [ 14 | "google", 15 | "drive", 16 | "database", 17 | "db", 18 | "table", 19 | "spreadsheet" 20 | ], 21 | "author": "Francisco Presencia Fandos", 22 | "licenses": [{ 23 | "type": "MIT", 24 | "url": "https://github.com/brentertz/scapegoat/blob/master/LICENSE-MIT" 25 | }], 26 | "bugs": { 27 | "url": "https://github.com/FranciscoP/drive-db/issues" 28 | }, 29 | "homepage": "https://github.com/FranciscoP/drive-db", 30 | "dependencies": { 31 | "request": "^2.51.0" 32 | }, 33 | "devDependencies": { 34 | "chai": "^1.10.0", 35 | "mocha": "^2.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Francisco Presencia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test/order.js: -------------------------------------------------------------------------------- 1 | // Load the testing module 2 | var should = require('chai').should(); 3 | 4 | // Load the class to test 5 | var drive = require('../index').load(); 6 | 7 | // Overload the data with a known set 8 | drive.data = require('./data.js'); 9 | 10 | // Retrieve the data 11 | var data = drive.find(); 12 | 13 | 14 | 15 | // Actual test 16 | 17 | // Attempt to update the cache 18 | describe('data.order(field)', function(){ 19 | 20 | // Retrieve the spreadsheet 21 | it('should sort by firstname', function(){ 22 | 23 | var people = data.order("firstname"); 24 | 25 | if (people[0].firstname > people[1].firstname 26 | || people[1].firstname > people[2].firstname 27 | || people[2].firstname > people[3].firstname 28 | || people[3].firstname > people[4].firstname) 29 | throw "Should be ordered ascendent"; 30 | }); 31 | }); 32 | 33 | // Attempt to update the cache 34 | describe('data.order(field, 1)', function(){ 35 | 36 | // Retrieve the spreadsheet 37 | it('should sort by age desc', function(){ 38 | 39 | var people = data.order("age", 1); 40 | 41 | if (people[0].age < people[1].age 42 | || people[1].age < people[2].age 43 | || people[2].age < people[3].age 44 | || people[3].age < people[4].age) 45 | throw "Should be ordered descendent"; 46 | }); 47 | }); -------------------------------------------------------------------------------- /test/update.js: -------------------------------------------------------------------------------- 1 | // Load the testing module 2 | var should = require('chai').should(); 3 | 4 | // Load the class to test 5 | var drive = require('../index').load(); 6 | 7 | // Overload the data with a known set 8 | drive.data = require('./data.js'); 9 | 10 | 11 | 12 | // Actual tests 13 | 14 | // Attempt to update the cache 15 | describe('drive.update(id, callback)', function(){ 16 | 17 | it('should update the db', function(done){ 18 | 19 | drive.load("test/db.json"); 20 | 21 | // Retrieve the spreadsheet 22 | drive.update("1BfDC-ryuqahvAVKFpu21KytkBWsFDSV4clNex4F1AXc", function(data){ 23 | done(); 24 | return data; 25 | }); 26 | }); 27 | 28 | it('should store an error', function(done){ 29 | 30 | drive.load("test/error.json"); 31 | 32 | // Retrieve the spreadsheet 33 | drive.update("wrong-id"); 34 | 35 | setTimeout(function(){ 36 | if(!drive.error) 37 | throw "Error not stored"; 38 | done(); 39 | }, 1500); 40 | }); 41 | 42 | // Check on the retrieved data 43 | after(function(){ 44 | 45 | // Retrieve the spreadsheet 46 | drive.load(); 47 | 48 | // Make sure we have some info 49 | if (drive.info.length === 0) 50 | throw "No info stored"; 51 | 52 | // Make sure there's something returned 53 | if (drive.data.length === 0) 54 | throw "No data loaded"; 55 | }); 56 | }); -------------------------------------------------------------------------------- /test/load.js: -------------------------------------------------------------------------------- 1 | // Load the testing module 2 | var should = require('chai').should(); 3 | 4 | // Load the class to test 5 | var drive = require('../index'); 6 | 7 | 8 | 9 | // Actual tests 10 | 11 | // Check if the passed object is a drive instance or not 12 | function checkDrive(obj) { 13 | 14 | // There's something 15 | if (!obj) 16 | throw "No database given"; 17 | 18 | // It's an object 19 | if (typeof obj !== "object") 20 | throw "drive should be an object"; 21 | 22 | // It's the right object 23 | if (!(obj instanceof drive.constructor)) 24 | throw "drive should be an instance of drive"; 25 | 26 | if (!(drive.hasOwnProperty("data"))) 27 | throw "drive should have a data parameter"; 28 | } 29 | 30 | 31 | // Load the DB from drive (local) 32 | describe('drive.load(filename)', function(){ 33 | 34 | // Make sure there's DB 35 | it('load db without filename', function(){ 36 | 37 | // Load the data 38 | drive.load(); 39 | 40 | // Check if drive is right 41 | checkDrive(drive); 42 | }); 43 | 44 | // Make sure there's DB 45 | it('load database default filename', function(){ 46 | 47 | // Load the data 48 | drive.load('db.json'); 49 | 50 | checkDrive(drive); 51 | }); 52 | 53 | // Make sure there's DB 54 | it('load database non-default filename', function(){ 55 | 56 | // Load the data 57 | drive.load('database.json'); 58 | 59 | checkDrive(drive); 60 | }); 61 | }); -------------------------------------------------------------------------------- /test/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "id": "1BfDC-ryuqahvAVKFpu21KytkBWsFDSV4clNex4F1AXc", 4 | "updated": 1423824820058, 5 | "error": "", 6 | "code": 200 7 | }, 8 | "data": [ 9 | { 10 | "id": "1", 11 | "firstname": "John", 12 | "lastname": "Smith", 13 | "age": "34", 14 | "city": "San Francisco", 15 | "country": "USA", 16 | "timestamp": "12/10/2010" 17 | }, 18 | { 19 | "id": "2", 20 | "firstname": "Mery", 21 | "lastname": "Johnson", 22 | "age": "19", 23 | "city": "Tokyo", 24 | "country": "Japan", 25 | "timestamp": "01/05/2003" 26 | }, 27 | { 28 | "id": "3", 29 | "firstname": "Peter", 30 | "lastname": "Williams", 31 | "age": "45", 32 | "city": "London", 33 | "country": "UK", 34 | "timestamp": "30/01/2007" 35 | }, 36 | { 37 | "id": "4", 38 | "firstname": "Laura", 39 | "lastname": "Brown", 40 | "age": "23", 41 | "city": "Madrid", 42 | "country": "Spain", 43 | "timestamp": "16/05/2008" 44 | }, 45 | { 46 | "id": "5", 47 | "firstname": "Jack", 48 | "lastname": "Jones", 49 | "age": "56", 50 | "city": "Paris", 51 | "country": "France", 52 | "timestamp": "07/03/2009" 53 | }, 54 | { 55 | "id": "6", 56 | "firstname": "Martha", 57 | "lastname": "Miller", 58 | "age": "73", 59 | "city": "Rome", 60 | "country": "Italy", 61 | "timestamp": "27/11/2005" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /test/limit.js: -------------------------------------------------------------------------------- 1 | // Load the testing module 2 | var should = require('chai').should(); 3 | 4 | // Load the class to test 5 | var drive = require('../index').load(); 6 | 7 | // Overload the data with a known set 8 | drive.data = require('./data.js'); 9 | 10 | // Retrieve all records 11 | var collection = drive.load("test/db.json").find(); 12 | 13 | 14 | 15 | // Actual test 16 | 17 | // Attempt to update the cache 18 | describe('data.limit(begin)', function(){ 19 | 20 | // Retrieve the spreadsheet 21 | it('retrieves everything from 2', function(){ 22 | 23 | var people = collection.limit(2); 24 | 25 | if (people.length !== 4) 26 | throw "Should retrieve only 4 people"; 27 | }); 28 | 29 | // Retrieve the spreadsheet 30 | it('retrieves the last 2 elements', function(){ 31 | 32 | var people = collection.limit(-2); 33 | 34 | if (people.length !== 2) 35 | throw "Should retrieve only 2 people"; 36 | }); 37 | }); 38 | 39 | 40 | 41 | // Attempt to update the cache 42 | describe('data.limit(begin, end)', function(){ 43 | 44 | // Retrieve the spreadsheet 45 | var collection = drive.load("test/db.json").find(); 46 | 47 | // Retrieve the spreadsheet 48 | it('retrieves the first 2 elements', function(){ 49 | 50 | var people = collection.limit(0, 2); 51 | 52 | if (people.length !== 2) 53 | throw "Should retrieve only 2 people"; 54 | }); 55 | 56 | // Retrieve the spreadsheet 57 | it('retrieves the from 4 to 6', function(){ 58 | 59 | var people = collection.limit(2, 6); 60 | 61 | if (people.length !== 4) 62 | throw "Should retrieve only 4 people"; 63 | }); 64 | }); -------------------------------------------------------------------------------- /test/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "id": "wrong-id", 4 | "updated": 1423824820459, 5 | "error": "No se encuentra la hoja de cálculo de esta dirección. Asegúrate de que tienes la dirección correcta y de que el usuario de la hoja de cálculo no la ha eliminado.", 6 | "code": 400 7 | }, 8 | "data": [ 9 | { 10 | "id": "1", 11 | "firstname": "John", 12 | "lastname": "Smith", 13 | "age": "34", 14 | "city": "San Francisco", 15 | "country": "USA", 16 | "timestamp": "12/10/2010" 17 | }, 18 | { 19 | "id": "2", 20 | "firstname": "Mery", 21 | "lastname": "Johnson", 22 | "age": "19", 23 | "city": "Tokyo", 24 | "country": "Japan", 25 | "timestamp": "01/05/2003" 26 | }, 27 | { 28 | "id": "3", 29 | "firstname": "Peter", 30 | "lastname": "Williams", 31 | "age": "45", 32 | "city": "London", 33 | "country": "UK", 34 | "timestamp": "30/01/2007" 35 | }, 36 | { 37 | "id": "4", 38 | "firstname": "Laura", 39 | "lastname": "Brown", 40 | "age": "23", 41 | "city": "Madrid", 42 | "country": "Spain", 43 | "timestamp": "16/05/2008" 44 | }, 45 | { 46 | "id": "5", 47 | "firstname": "Jack", 48 | "lastname": "Jones", 49 | "age": "56", 50 | "city": "Paris", 51 | "country": "France", 52 | "timestamp": "07/03/2009" 53 | }, 54 | { 55 | "id": "6", 56 | "firstname": "Martha", 57 | "lastname": "Miller", 58 | "age": "73", 59 | "city": "Rome", 60 | "country": "Italy", 61 | "timestamp": "27/11/2005" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /test/find.js: -------------------------------------------------------------------------------- 1 | // Load the testing module 2 | var should = require('chai').should(); 3 | 4 | // Load the class to test 5 | var drive = require('../index').load(); 6 | 7 | // Overload the data with a known set 8 | drive.data = require('./data.js'); 9 | 10 | 11 | 12 | // Actual tests 13 | 14 | // Find all data 15 | describe('drive.find()', function(){ 16 | 17 | // Retrieve the spreadsheet 18 | it('should load all records', function(){ 19 | if (drive.find().length !== 6) 20 | throw "Not all records were retrieved"; 21 | }); 22 | }); 23 | 24 | 25 | 26 | // Attempt to update the cache 27 | describe('drive.find(filter)', function(){ 28 | 29 | // Retrieve the spreadsheet 30 | drive.load(); 31 | 32 | // Retrieve the spreadsheet 33 | it('should load first record', function(){ 34 | if (drive.find({ id: 1 }).length !== 1) 35 | throw "Only one record should be found"; 36 | }); 37 | 38 | // Retrieve the spreadsheet 39 | it('should load John record', function(){ 40 | if (drive.find({ firstname: "John" }).length !== 1) 41 | throw "Only one record should be found"; 42 | }); 43 | 44 | // Retrieve the spreadsheet 45 | it('should load Miller record', function(){ 46 | if (drive.find({ lastname: "Miller" }).length !== 1) 47 | throw "Only one record should be found"; 48 | }); 49 | }); 50 | 51 | 52 | 53 | // Attempt to update the cache 54 | describe('drive.find(complexfilter)', function(){ 55 | 56 | // Retrieve the spreadsheet 57 | drive.load(); 58 | 59 | // Retrieve the spreadsheet 60 | it('should load records with id > 4', function(){ 61 | var records = drive.find({ id: {$gt: 4} }); 62 | var none = records.filter(function(row){ return row.id <= 4; }); 63 | if (none.length > 0) 64 | throw "There's some record smaller than 4"; 65 | }); 66 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # drive-db 2 | 3 | A Google Drive spreadsheet simple database. Stop wasting your time when a simple table is enough. Perfect for collaboration with multiple people editing the same spreadsheet. 4 | 5 | 6 | 7 | ## Usage 8 | 9 | The database is stored locally and updated whenever you want from the spreadsheet. For detailed documentation read documentation.md, but it's really easy to use: 10 | 11 | // Include the module and load the data from the default local cache 12 | var drive = require("drive-db").load(); 13 | 14 | // Retrieve all the people named `John` 15 | var Johns = drive.find({ firstname: "John" }); 16 | 17 | 18 | To update the data asynchronously, call next code. Update it whenever you want, after the `.load()` or each X seconds/minutes/hours: 19 | 20 | // Update the local data (async) 21 | drive.update("1BfDC-ryuqahvAVKFpu21KytkBWsFDSV4clNex4F1AXc"); 22 | 23 | You can perform `find()` queries like mongoDB's [comparison query operators](http://docs.mongodb.org/manual/reference/operator/query-comparison/) after calling `.load()`: 24 | 25 | // Return an array with one element that has the id 3 26 | drive.find({ id: 3 }); 27 | 28 | // Return an array of people called "John" or "Jack" 29 | drive.find({ firstname: { $in: ["John", "Jack"]] } }); 30 | 31 | // Return an array with everyone but "John" 32 | drive.find({ firstname: { $ne: "John" } }); 33 | 34 | 35 | ## Installation 36 | 37 | npm install drive-db --save 38 | 39 | To get the right google drive spreadsheet: 40 | 41 | - Create a spreadsheet 42 | - File > Publish to the Web > Publish 43 | - Copy the id between `/spreadsheets/` and `/edit` in the url: 44 | 45 | > [https://docs.google.com/spreadsheets/d/1fvz34wY6phWDJsuIneqvOoZRPfo6CfJyPg1BYgHt59k/edit#gid=0](https://docs.google.com/spreadsheets/d/1fvz34wY6phWDJsuIneqvOoZRPfo6CfJyPg1BYgHt59k/edit#gid=0) 46 | 47 | - Use this inside `update()` 48 | 49 | drive.update("1fvz34wY6phWDJsuIneqvOoZRPfo6CfJyPg1BYgHt59k"); 50 | 51 | Note: the table has to have a structure similar to this, where the first row are the alphanumeric field names: 52 | 53 | | id | firstname | lastname | age | city | 54 | |----|-----------|----------|-----|---------------| 55 | | 1 | John | Smith | 34 | San Francisco | 56 | | 2 | Mery | Johnson | 19 | Tokyo | 57 | | 3 | Peter | Williams | 45 | London | 58 | 59 | See [this document](https://docs.google.com/spreadsheets/d/1fvz34wY6phWDJsuIneqvOoZRPfo6CfJyPg1BYgHt59k/edit#gid=0) as an example 60 | 61 | 62 | ## Test 63 | 64 | To run the tests, simply call: 65 | 66 | npm test 67 | 68 | ## Contributing 69 | 70 | Please take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code. 71 | 72 | Areas where I'm seeking for help: 73 | 74 | - Testing. Adding coverage or improving existing ones. 75 | - Documentation. Make everything clear. 76 | 77 | 78 | ## Release history 79 | 80 | - 2.0 [future] finish battle testing it, full test coverage and proper documentation. Delete old code. 81 | - ... 82 | - 1.6 Added the `limit()` function. Tests are modular and easy to do/deploy 83 | - 1.5 Gave the `info` to the global object instead of a sub-object. Stored the error and code from the update in the db. Added the method `order()` for the array from `find()`. 84 | - 1.4 Changed the way it works internally from url to spreadsheet id. 85 | - 1.3 Stopped `require('drive-db')` from calling `.load()` automatically. The DB might me elsewhere. There's always an `.after` function. 86 | - 1.2 Changed several things. Created `documentation.md`, which should be up to date to keep up with the changes. 87 | - 1.1 Changed the parameter inside `load()`. Now it's the file where the cache is stored. 88 | - 1.0 First release 89 | 90 | 91 | ## Thanks to 92 | 93 | - [Creating and publishing a node.js module](https://quickleft.com/blog/creating-and-publishing-a-node-js-module/) 94 | -------------------------------------------------------------------------------- /documentation.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | 4 | 5 | ## Installation 6 | 7 | Go to your project folder and install it with: 8 | 9 | npm install drive-db --save 10 | 11 | With the `--save` flag you add it to the dependencies so you can uncheck `node_modules` from git. 12 | 13 | 14 | 15 | ## Include the module 16 | 17 | var drive = require('drive-db'); 18 | 19 | With this command you can include it straight away. All other methods below require you to include the module properly. Of course, you can call the module `drive` or `db`. The module name, `drive-db`, comes from the fact that I kept mixing both of the names in the code. 20 | 21 | However we recommend you to do this. Read the rationale in the following point: 22 | 23 | var drive = require('drive-db').load(); 24 | 25 | 26 | ## .load([filename]) 27 | 28 | With this command you load the local database from its default location, `db.json`: 29 | 30 | drive.load(); 31 | 32 | This is the same as doing: 33 | 34 | drive.load('db.json'); 35 | 36 | However, if you have more than one table or you just want to put it in a different place or with a different name, you can do so easily: 37 | 38 | drive.load('db/drive.json'); 39 | 40 | 41 | 42 | ## .update(id[, afterupdate]) 43 | 44 | Retrieves the google drive spreadsheet asynchronously, process it and store it locally. It needs at least the google drive id as first parameter, and it accepts a callback that will be processed afterwards. An example: 45 | 46 | drive.update("1fvz34wY6phWDJsuIneqvOoZRPfo6CfJyPg1BYgHt59k"); 47 | 48 | Another example: 49 | 50 | drive.update("1BfDC-ryuqahvAVKFpu21KytkBWsFDSV4clNex4F1AXc", function(data){ 51 | console.log("There are " + data.length + " rows"); 52 | return data; 53 | }); 54 | 55 | Yet another one: 56 | 57 | drive.update("1fvz34wY6phWDJsuIneqvOoZRPfo6CfJyPg1BYgHt59k", function(data){ 58 | data.forEach(function(row){ 59 | row.fullname = row.firstname + " " + row.lastname; 60 | }); 61 | return data; 62 | }); 63 | 64 | Note that, if you call `.update(id)` and the file doesn't exist yet, it will be created. 65 | 66 | 67 | ## .find([filter]) 68 | 69 | Retrieve data from the database. If there's no filter, the whole spreadsheet will be retrieved. It behaves in the same way as mongoDB's [comparison query operators](http://docs.mongodb.org/manual/reference/operator/query-comparison/), so you can read the documentation there. Returns a javascript Array with the extra methods `.order()` and (not yet) `.limit(begin, end)`. So, the documentation is here: 70 | 71 | > **[mongoDB comparison query operators](http://docs.mongodb.org/manual/reference/operator/query-comparison/)** 72 | 73 | 74 | ## .order(field[, desc]) 75 | 76 | > This has been called `order()` instead of `sort()` as a javascript Array already has a native method called `sort()` which works quite diferent. 77 | 78 | Sort the data by the given field. It sorts it in an ascendant order. Pass a second parameter as true and it will sort it in a descendant order. It should be called **after** `.find()`. Examples: 79 | 80 | // Ascendant order 81 | var people = drive.find().order('firstname'); 82 | 83 | // Descedant order 84 | var inversepeople = drive.find().order('firstname', true); 85 | 86 | var smiths = drive.find({ lastname: "Smith" }).order('firstname'); 87 | 88 | 89 | 90 | ## .limit(begin, end) 91 | 92 | > An alternative name to the native `.slice()`. The [Mozilla documentation](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) is pretty neat. I created this to make it consistent to database manipulation names. 93 | 94 | Limit the data that can be retrieved. It should be applied to the returning array from `.find()`, and not before: 95 | 96 | // Limit the set to the first 10 elements 97 | drive.find().limit(0, 10); 98 | 99 | // Retrieve the next 10 elements (pagination, infinite scroll, etc) 100 | drive.find().limit(10, 20); 101 | 102 | // Retrieves the last 2 elements 103 | drive.find().limit(-2); 104 | 105 | // Order the query and limit it 106 | drive.find().sort("firstname").limit(0, 10); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Required modules 2 | var fs = require('fs'); 3 | var request = require('request'); 4 | 5 | 6 | 7 | /** 8 | * Drive Model 9 | * Use a Google Drive sheet as a database with strong cache 10 | */ 11 | var drive = function (){}; 12 | 13 | 14 | 15 | /** 16 | * Cache Path 17 | * Path where a local copy will be stored 18 | */ 19 | drive.prototype.cachePath = 'db.json'; 20 | 21 | 22 | 23 | /** 24 | * id 25 | * The id of the Spreadsheet 26 | */ 27 | drive.prototype.id = ''; 28 | 29 | 30 | 31 | /** 32 | * Info 33 | * Variable that contains the database information 34 | */ 35 | drive.prototype.info = {}; 36 | 37 | 38 | 39 | /** 40 | * Data 41 | * This variable will contain the database data 42 | */ 43 | drive.prototype.data = []; 44 | 45 | 46 | /** 47 | * Loaded 48 | * Variable set to true if the database is loaded 49 | */ 50 | drive.prototype.loaded = false; 51 | 52 | 53 | /** 54 | * Error 55 | * Variable to store the database errors 56 | */ 57 | drive.prototype.error = false; 58 | 59 | 60 | /** 61 | * Loaded 62 | * Variable with the response code 63 | */ 64 | drive.prototype.code = 0; 65 | 66 | 67 | 68 | /** 69 | * Load 70 | * Retrieve the data from the local copy 71 | * @param String cachePath the place where the local copy is stored 72 | */ 73 | drive.prototype.load = function(cachePath){ 74 | 75 | // Set the cachePath 76 | this.cachePath = cachePath ? cachePath : this.cachePath; 77 | 78 | // If there's no local DB 79 | if(!fs.existsSync(this.cachePath)) { 80 | return this; 81 | } 82 | 83 | // Read the raw db into a variable 84 | var rawJson = fs.readFileSync(this.cachePath, 'utf-8'); 85 | 86 | // Store it in a decent way 87 | try { 88 | var db = JSON.parse(rawJson); 89 | this.data = db.data; 90 | for (var key in db.info) { 91 | this[key] = db.info[key]; 92 | } 93 | this.loaded = true; 94 | } 95 | catch(error) { 96 | console.log('Error reading from local db.'); 97 | } 98 | 99 | return this; 100 | }; 101 | 102 | 103 | 104 | /** 105 | * Update 106 | * Refresh the Google Spreadsheet data into local database 107 | * @param id Google Drive Spreadsheet id 108 | * @param callback the function to call after the data is retrieved 109 | */ 110 | drive.prototype.update = function(id, callback){ 111 | 112 | // Store the id from google drive spreadsheet 113 | this.id = id || this.id; 114 | 115 | // The function to be called after the data is loaded 116 | this.after = callback || this.after; 117 | 118 | // To update the data we need to make sure we're working with an id 119 | if (!this.id.length) 120 | throw 'Need a google drive url to update file'; 121 | 122 | // Build the url 123 | var url = 'https://spreadsheets.google.com/feeds/list/' + id + '/od6/public/values?alt=json'; 124 | 125 | // http://stackoverflow.com/questions/962033/what-underlies-this-javascript-idiom-var-self-this 126 | var self = this; 127 | 128 | // Call request() but keep this as `drive` 129 | request(url, function(error, response, sheet){ 130 | 131 | // Store the response code 132 | // 400 if there's no response at all (client error) 133 | self.code = (response) ? response.statusCode : 400; 134 | 135 | // If it's an error code 136 | if (self.code >= 400) { 137 | self.error = (response) ? response.body : "No internet connection"; 138 | self.store(); 139 | return false; 140 | } 141 | 142 | self.error = ""; 143 | 144 | // So that you can access this within self.after 145 | self.data = self.parse(sheet); 146 | 147 | // Call the function that should be called after retrieving the data 148 | self.data = self.after.call(self, self.data); 149 | 150 | // Actually save the data into the file 151 | self.store(); 152 | }); 153 | }; 154 | 155 | 156 | 157 | /** 158 | * After 159 | * The function to call to process the data 160 | */ 161 | drive.prototype.after = function(data){ 162 | return data; 163 | } 164 | 165 | 166 | 167 | /** 168 | * Store 169 | * Save the current data into the db 170 | */ 171 | drive.prototype.store = function(){ 172 | 173 | // Store when it is last updated 174 | this.updated = new Date().getTime(); 175 | 176 | // The data to store 177 | var save = JSON.stringify({ 178 | info: { 179 | id: this.id, 180 | updated: this.updated, 181 | error: this.error, 182 | code: this.code 183 | }, 184 | data: this.data 185 | }, null, 2); 186 | 187 | // Write the cache 188 | fs.writeFile(this.cachePath, save); 189 | }; 190 | 191 | 192 | 193 | /** 194 | * Parse method 195 | * Transforms Google Drive raw data into something usable 196 | */ 197 | drive.prototype.parse = function(raw) { 198 | 199 | // Get the json from google drive 200 | var rawrows = JSON.parse(raw).feed.entry; 201 | 202 | // Loop through each row 203 | var data = rawrows.map(function(row){ 204 | 205 | var entry = {}; 206 | 207 | // Loop through all of the fields (only some are valid) 208 | for (var field in row) { 209 | 210 | // Match only those field names that are valid 211 | if (field.match(/gsx\$[0-9a-zA-Z]+/)) { 212 | 213 | // Get the field real name 214 | var name = field.match(/gsx\$([0-9a-zA-Z]+)/)[1]; 215 | 216 | // Store it and its value 217 | entry[name] = row[field].$t; 218 | } 219 | } 220 | 221 | // Return it anyway 222 | return entry; 223 | }); 224 | 225 | return data; 226 | }; 227 | 228 | 229 | 230 | /** 231 | * Each 232 | * Loop through all of the elements and execute an action 233 | * You can call `this` from the callback and it'll be nice 234 | */ 235 | drive.prototype.each = function(fn){ 236 | this.data.forEach(fn, this); 237 | return this; 238 | }; 239 | 240 | 241 | 242 | /** 243 | * Clean 244 | * Sanitize the data by deleting empty stuff 245 | * If we need to clean it means we mess up. Fix it somehow 246 | */ 247 | drive.prototype.clean = function(){ 248 | 249 | if (!this.data || this.data.constructor !== Array) 250 | this.data = []; 251 | 252 | this.data = this.data.filter(function(n){ 253 | return n !== undefined; 254 | }); 255 | }; 256 | 257 | 258 | 259 | /** 260 | * Conditions 261 | * The different mongodb conditions 262 | * @src http://docs.mongodb.org/manual/reference/operator/query-comparison/ 263 | */ 264 | var conditions = { 265 | // This one is not actually in mongodb, but it's nice 266 | "$eq" : function(value, test){ return value == test; }, 267 | "$gt" : function(value, test){ return value > test; }, 268 | "$gte": function(value, test){ return value >= test; }, 269 | "$lt" : function(value, test){ return value < test; }, 270 | "$lte": function(value, test){ return value <= test; }, 271 | "$ne" : function(value, test){ return value != test; }, 272 | // http://stackoverflow.com/a/20206734 273 | "$in" : function(value, test){ 274 | return test.map(String).indexOf(value) > -1; 275 | }, 276 | "$nin": function(value, test){ 277 | return !conditions.$in(value, test); 278 | }, 279 | }; 280 | 281 | 282 | 283 | // From http://docs.mongodb.org/manual/reference/operator/query/ 284 | function good(value, test){ 285 | 286 | // Comparing two primitive types 287 | if (typeof test !== 'object') { 288 | return (test == value); 289 | } 290 | 291 | // Loop each possible condition 292 | for (var name in conditions) { 293 | 294 | // If the filter has this test and it's not passed 295 | if (conditions.hasOwnProperty(name) && 296 | test.hasOwnProperty(name) && 297 | !conditions[name](value, test[name])) { 298 | return false; 299 | } 300 | } 301 | 302 | // All the tests for the complex filter have passed 303 | return true; 304 | } 305 | 306 | 307 | 308 | // Find one instance 309 | // Filter: { id: 'bla' } | 'bla' | null 310 | drive.prototype.find = function(filter) { 311 | 312 | // Allow for simplification when calling it 313 | filter = (typeof filter == 'string') ? { id: filter } : filter; 314 | 315 | // Make sure we're working with a clean array 316 | this.clean(); 317 | 318 | // Loop through all of the rows 319 | // Store the good ones here 320 | var passed = this.data.map(function(row){ 321 | 322 | // Loop through all of the tests 323 | for (var field in filter) { 324 | 325 | // Make sure we're dealing with a filter field 326 | if (filter.hasOwnProperty(field)) { 327 | 328 | // If one of the tests fails 329 | if (!good(row[field], filter[field])) { 330 | 331 | // The whole row fails 332 | return false; 333 | } 334 | } 335 | } 336 | 337 | // Everything okay: this row passed all the tests! 338 | return row; 339 | }); 340 | 341 | // http://stackoverflow.com/a/2843625 342 | passed = passed.filter(function(row){ 343 | return (row !== undefined && row !== null && row !== false); 344 | }); 345 | 346 | // This has been called `order` since Array already has a function called `sort` 347 | passed.order = function(field, desc){ 348 | 349 | // Inverse the order of the sort 350 | var inv = (desc) ? -1 : 1; 351 | 352 | // Compare two fields 353 | function compare(a, b) { 354 | 355 | return (a[field] == b[field]) ? 0 : 356 | (a[field] > b[field]) ? inv : - inv; 357 | } 358 | 359 | // Actually sort the data 360 | this.sort(compare); 361 | 362 | return this; 363 | } 364 | 365 | // Define .limit() as .slice() 366 | passed.limit = passed.slice; 367 | 368 | return passed; 369 | }; 370 | 371 | 372 | 373 | // Sort the data by a field 374 | // src: http://stackoverflow.com/a/1129270/938236 375 | drive.prototype.sort = function(field, desc){ 376 | 377 | // Inverse the order of the sort 378 | var inv = (desc) ? -1 : 1; 379 | 380 | // Compare two fields 381 | function compare(a, b) { 382 | 383 | return (a[field] == b[field]) ? 0 : 384 | (a[field] > b[field]) ? inv : - inv; 385 | } 386 | 387 | // Actually sort the data 388 | this.data.sort(compare); 389 | 390 | return this; 391 | }; 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | // Get the next element 401 | drive.prototype.next = function(id){ 402 | 403 | // Store the good ones 404 | var passed = this.data; 405 | 406 | var matched = false; 407 | var good; 408 | 409 | // Loop through all of the rows 410 | passed.forEach(function(row){ 411 | if (matched) { 412 | good = row; 413 | matched = false; 414 | } 415 | if (row && row.id == id) 416 | matched = true; 417 | }); 418 | 419 | return good; 420 | }; 421 | 422 | 423 | module.exports = new drive(); --------------------------------------------------------------------------------