From 159f72fe3d1fd37eb4a6e99e359fe4415b4ac934 Mon Sep 17 00:00:00 2001 From: Fernando Blat Date: Thu, 9 Jun 2011 18:34:02 +0200 Subject: [PATCH] First version of the API server with some TODOs to solve --- .gitignore | 4 + Makefile | 11 +++ app.js | 82 +++++++++++++++++++ cluster.js | 13 +++ step.js | 154 ++++++++++++++++++++++++++++++++++++ test/acceptance/app.test.js | 22 ++++++ test/unit/app.test.js | 1 + 7 files changed, 287 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100755 app.js create mode 100755 cluster.js create mode 100644 step.js create mode 100644 test/acceptance/app.test.js create mode 100644 test/unit/app.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..df01e840 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +logs/ +pids/ +*.sock +test/tmp/* \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..a3ed3872 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +test-tmp: + @rm -rf test/tmp + @mkdir -p test/tmp + +test: + expresso -I lib test/unit/*.js test/acceptance/*.js + +test-cov: + expresso -I lib --cov test/unit/*.js test/acceptance/*.js + +.PHONY: test test-cov diff --git a/app.js b/app.js new file mode 100755 index 00000000..35b4572b --- /dev/null +++ b/app.js @@ -0,0 +1,82 @@ +#! /Users/fernando/local/node/bin/node + +var app = require('express').createServer() + , pg = require('pg').native + , Step = require('./step') + , _ = require('underscore') + , redis = require("redis") + , redisClient = redis.createClient(); + +redisClient.select(3); +_.mixin(require('underscore.string')); + +function authenticate(req){ + var oauth_token; + // Parameter oauth_token with the OAuth token + if (!_.isUndefined(req.query.oauth_token)){ + oauth_token = _.trim(req.query.oauth_token); + } else if (!_.isUndefined(req.headers.authorization)) { + // OAuth token is in the header Authorization + oauth_token = req.headers.authorization.match(/oauth_token=\"([^\"]+)\"/)[1] + } + if (!_.isUndefined(oauth_token)) { + return oauth_token; + } +} + +app.get('/v1/', function(req, res){ + if (!_.isUndefined(req.query.sql)){ + var sql = _.trim(req.query.sql); + var conString; + Step( + function getUser(err) { + var oauth_token = authenticate(req); + console.log("oauth_token: " + oauth_token); + if (!_.isUndefined(oauth_token)) { + redisClient.hget("rails:oauth_tokens:" + oauth_token, "user_id", this); + } else { + connectDb(undefined,undefined); + } + console.log("next step!"); + }, + function connectDb(error, reply){ + if(!_.isUndefined(reply)) { + var user_id = reply.toString(); + // TODO: + // - database_username: depending on the environment is different: + // - development: dev_cartodb_user_33 + // - test: test_cartodb_user_33 + // - production: cartodb_user_33 + // - database_name: depending on the environment is different: + // - development: cartodb_dev_user_33_db + // - test: cartodb_test_user_33_db + // - production: cartodb_user_33_db + var database_username = "cartodb_user_" + user_id; + var database_name = "cartodb_dev_user_" + user_id + "_db" + conString = "tcp://" + database_username + "@localhost/" + database_name; + } else { + // TODO: + // - if the user doesn't identify who is how do we now to which database to connect? + // I think it's impossible to know unless each request goes to a different subdomain + // depending on the user + conString = "tcp://publicuser@localhost/cartodb_dev_user_2_db"; + } + pg.connect(conString, this); + }, + function queryDb(err, client){ + if (err) throw err; + client.query(sql, this); + }, + function setResultToBrowser(err, result){ + if (err) + res.send({error:[err.message]}, 400); + else + res.send(result.rows); + } + ); + } else { + res.send({error:["You must indicate a sql query"]}, 400); + } +}); + +module.exports = app; diff --git a/cluster.js b/cluster.js new file mode 100755 index 00000000..8b60fd2e --- /dev/null +++ b/cluster.js @@ -0,0 +1,13 @@ +#! /Users/fernando/local/node/bin/node + +var cluster = require('cluster'); + +cluster('./app') + .use(cluster.logger('logs')) + .set('workers', 2) + .use(cluster.stats()) + .use(cluster.pidfiles('pids')) + .use(cluster.cli()) + .use(cluster.repl(8888)) + .use(cluster.debug()) + .listen(3000); diff --git a/step.js b/step.js new file mode 100644 index 00000000..546c14dd --- /dev/null +++ b/step.js @@ -0,0 +1,154 @@ +/* +Copyright (c) 2011 Tim Caswell + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Inspired by http://github.com/willconant/flow-js, but reimplemented and +// modified to fit my taste and the node.JS error handling system. +function Step() { + var steps = Array.prototype.slice.call(arguments), + pending, counter, results, lock; + + // Define the main callback that's given as `this` to the steps. + function next() { + + // Check if there are no steps left + if (steps.length === 0) { + // Throw uncaught errors + if (arguments[0]) { + throw arguments[0]; + } + return; + } + + // Get the next step to execute + var fn = steps.shift(); + counter = pending = 0; + results = []; + + // Run the step in a try..catch block so exceptions don't get out of hand. + try { + lock = true; + var result = fn.apply(next, arguments); + } catch (e) { + // Pass any exceptions on through the next callback + next(e); + } + + + // If a syncronous return is used, pass it to the callback + if (result !== undefined) { + next(undefined, result); + } + lock = false; + } + + // Add a special callback generator `this.parallel()` that groups stuff. + next.parallel = function () { + var index = 1 + counter++; + pending++; + + function check() { + if (pending === 0) { + // When they're all done, call the callback + next.apply(null, results); + } + } + process.nextTick(check); // Ensures that check is called at least once + + return function () { + pending--; + // Compress the error from any result to the first argument + if (arguments[0]) { + results[0] = arguments[0]; + } + // Send the other results as arguments + results[index] = arguments[1]; + if (!lock) { check(); } + }; + }; + + // Generates a callback generator for grouped results + next.group = function () { + var localCallback = next.parallel(); + var counter = 0; + var pending = 0; + var result = []; + var error = undefined; + + function check() { + if (pending === 0) { + // When group is done, call the callback + localCallback(error, result); + } + } + process.nextTick(check); // Ensures that check is called at least once + + // Generates a callback for the group + return function () { + var index = counter++; + pending++; + return function () { + pending--; + // Compress the error from any result to the first argument + if (arguments[0]) { + error = arguments[0]; + } + // Send the other results as arguments + result[index] = arguments[1]; + if (!lock) { check(); } + }; + }; + }; + + // Start the engine an pass nothing to the first step. + next(); +} + +// Tack on leading and tailing steps for input and output and return +// the whole thing as a function. Basically turns step calls into function +// factories. +Step.fn = function StepFn() { + var steps = Array.prototype.slice.call(arguments); + return function () { + var args = Array.prototype.slice.call(arguments); + + // Insert a first step that primes the data stream + var toRun = [function () { + this.apply(null, args); + }].concat(steps); + + // If the last arg is a function add it as a last step + if (typeof args[args.length-1] === 'function') { + toRun.push(args.pop()); + } + + + Step.apply(null, toRun); + } +} + + +// Hook into commonJS module systems +if (typeof module !== 'undefined' && "exports" in module) { + module.exports = Step; +} diff --git a/test/acceptance/app.test.js b/test/acceptance/app.test.js new file mode 100644 index 00000000..d4fb745f --- /dev/null +++ b/test/acceptance/app.test.js @@ -0,0 +1,22 @@ +var app = require('../../app') + , assert = require('assert'); + +module.exports = { + 'GET /v1/': function(){ + assert.response(app, { + url: '/v1/', + method: 'GET' + },{ + body: '{"error":["You must indicate a sql query"]}', + status: 400 + }); + }, + 'GET /v1/ with SQL parameter': function(){ + assert.response(app, { + url: '/v1/?sql=bla', + method: 'GET' + },{ + status: 200 + }); + } +}; \ No newline at end of file diff --git a/test/unit/app.test.js b/test/unit/app.test.js new file mode 100644 index 00000000..a3d114eb --- /dev/null +++ b/test/unit/app.test.js @@ -0,0 +1 @@ +// Empty file but necessary to make `make test` won't fail \ No newline at end of file