var express = require("express") , passport = require("passport") , passportLocal = require("passport-local") , fs = require("fs") , path = require("path") , proxy = require("./util/simple-http-proxy") , CSV = require("ya-csv") , CouchDB = require('./util/couch').CouchDB , _ = require("underscore") , tv4 = require("tv4") ; function assertSetting(name, settings, validate) { if ( typeof settings[name] === 'undefined' ) { throw new Error("Required setting '"+name+"' undefined."); } if ( _.isFunction(validate) && !validate(settings[name]) ) { throw new Error("Setting '"+name+"' with value '"+settings[name]+"' is invalid."); } } exports.App = function(settings) { assertSetting("couchDbURL", settings, _.isString); var couch = new CouchDB(settings.couchDbURL); var schema = require("./config/couchdb-schema.json"); return couch.get("schemaInfo").then(function(schemaInfo){ if (schemaInfo.version !== schema.version) { var msg = "Database has version "+schemaInfo.version+" but we expect version "+schema.version; throw new Error(msg); } return configureApp(settings,couch,schema); }); }; function configureApp(settings,couch,schema) { function clientPath(relativePath) { return path.resolve(__dirname+'/../client/'+relativePath); } passport.use(new passportLocal.Strategy(function(username, password, done){ if ( username === "igor" && password === "mayer" ) { done(null,{ username: "igor" }); } else { done(null,false,{ message: 'Invalid credentials.' }); } })); passport.serializeUser(function(user, done) { done(null, user.username); }); passport.deserializeUser(function(id, done) { done(null, {username: id}); }); var app = express(); app.use(express.logger()); app.use(express.compress()); app.use(express.favicon()); // cookies and session app.use(express.cookieParser()); app.use(express.session({ secret: "quasi experimental design" })); app.use('/api',express.bodyParser()); // passport app.use(passport.initialize()); app.use(passport.session()); function ensureAuthenticated(req,res,next){ if (!req.user) { return res.send(401,{error:"Login before accessing API."}); } else { return next(); } } function returnUser(req,res) { res.send(200, req.user); } // static resources app.get('/', function(request, response){ response.sendfile(clientPath('index.html')); }); app.get('/*.html', function(request, response) { response.sendfile(clientPath(request.path)); }); _.each(['/dojo', '/dijit', '/dojox', '/qed', '/qed-client'], function(dir){ app.use(dir, express.static(clientPath(dir))); }); // post to this url to login app.post( '/api/login', passport.authenticate('local'), returnUser); // return the info for the current logged in user app.get( '/api/login', ensureAuthenticated, returnUser); // explicitly logout this user app.post( '/api/logout', ensureAuthenticated, function(req,res){ req.logout(); res.send(200,{}); }); function makeDocsGet(type) { return function(req,res) { var url = '_design/default/_view/by_type?key='+JSON.stringify(type); couch.get(url).then(function(result){ var items = _.map(result.rows, function(item) { return item.value; }); res.send(200, items); }, function(error){ res.send(404, {error: "Cannot find collection"}); }); }; } function makeDocGet_id(type) { return function(req,res) { var id = req.params.id; couch.get(id).then(function(result){ if ( result.type === type ) { res.send(200, result); } else { res.send(404, {error: "Document "+id+" is not a "+type}); } }, function(error){ res.send(404, {error: "Cannot find survey run "+id}); }); }; } function makeDocPut_id(type) { return function(req,res) { var id = req.params.id; var doc = req.body; if ( doc.type === type && tv4.validateResult(doc, schema) ) { couch.put(id,doc).then(function(result){ res.send(200, result); }, function(error){ res.send(500, error); }); } else { res.send(400,{error: "Document failed schema verification."}); } }; } function makeDocDel_id(type) { return function(req,res) { var id = req.params.id; var rev = req.query.rev; couch.delete(id,rev).then(function(result){ res.send(200, result); }, function(error){ res.send(500, error); }); }; } function makeDocPost(type) { return function(req,res) { var doc = req.body; if ( doc.type === type && tv4.validateResult(doc, schema) ) { couch.post(req.body).then(function(result){ res.send(200, result); }, function(error){ res.send(500, error); }); } else { res.send(400,{error: "Document failed schema verification."}); } }; } function notImplemented(req,res) { res.send(501,{error:"API not implemented yet."}); } // Questions app.get('/api/questions', ensureAuthenticated, makeDocsGet('Question')); app.get('/api/questions/:id', ensureAuthenticated, makeDocGet_id('Question')); app.post('/api/questions', ensureAuthenticated, makeDocPost('Question')); app.put('/api/questions/:id', ensureAuthenticated, makeDocPut_id('Question')); app.delete('/api/questions/:id', ensureAuthenticated, makeDocDel_id('Question')); app.get('/api/questions/categories', ensureAuthenticated, notImplemented); // Surveys app.get('/api/surveys', ensureAuthenticated, makeDocsGet('Survey')); app.get('/api/surveys/:id', ensureAuthenticated, makeDocGet_id('Survey')); app.post('/api/surveys', ensureAuthenticated, makeDocPost('Survey')); app.put('/api/surveys/:id', ensureAuthenticated, makeDocPut_id('Survey')); app.delete('/api/surveys/:id', ensureAuthenticated, makeDocDel_id('Survey')); // SurveyRuns app.get('/api/surveyRuns', ensureAuthenticated, makeDocsGet('SurveyRun')); app.get('/api/surveyRuns/:id', ensureAuthenticated, makeDocGet_id('SurveyRun')); app.get('/api/surveyRuns/:id/responses.csv', ensureAuthenticated, function(req, res) { var id = req.params.id; var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(id); couch.get(url).then(function(result){ var responses = _.map(result.rows, function(item) { return item.value; }); var flatResponses = responsesToVariables(responses); res.set({ 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename=surveyRun-'+id+'-responses.csv' }); res.status(200); writeObjectsToCSVStream(flatResponses, res); res.end(); }, function(error){ res.send(404, {error: "Cannot find responses for survey run "+id}); }); }); app.get('/api/surveyRuns/:id/responses', ensureAuthenticated, function(req,res) { var id = req.params.id; var url = '_design/responses/_view/by_surveyrun?key='+JSON.stringify(id); couch.get(url).then(function(result){ var responses = _.map(result.rows, function(item) { return item.value; }); res.send(200, responses); }, function(error){ res.send(404, {error: "Cannot find responses for survey run "+id}); }); }); // Responses app.get('/api/responses', ensureAuthenticated, makeDocsGet('Response')); app.get('/api/responses/:id', ensureAuthenticated, makeDocGet_id('Response')); app.post('/api/responses', ensureAuthenticated, makeDocPost('Response')); app.put('/api/responses/:id', ensureAuthenticated, makeDocPut_id('Response')); app.delete('/api/responses/:id', ensureAuthenticated, makeDocDel_id('Response')); return app; } function responsesToVariables(responses) { return _.map(responses, responseToVariables); } function responseToVariables(response) { var result = flattenObject(response.answers); return result; } function flattenObject(value) { var result = {}; (function agg(val,key,res){ if ( _.isObject(val) ) { var keys = _.keys(val); // FIXME : dirty hack for questions with only one input if ( keys.length === 1 ) { agg(val[keys[0]],key,res); } else { _.forEach(val, function(v,k){ agg(v,(key ? key+'.' : '')+k,res); }); } } else if ( _.isArray(val) ) { // FIXME : dirty hack for questions with only one input if ( val.length === 1 ) { agg(val[0],key,res); } else { _.forEach(val, function(v,i){ agg(v,(key ? key+'.' : '')+i,res); }); } } else { res[key] = val; } })(value,null,result); return result; } function writeObjectsToCSVStream(objects, stream, prelude) { var keys = _.chain(objects) .map(_.keys) .flatten() .uniq() .value(); var idxs = {}; _.forEach(keys, function(key,idx){ idxs[key] = idx; }); var writer = new CSV.CsvWriter(stream); if ( prelude ) { _.forEach(prelude, function(val,key){ writer.writeRecord([key,val]); }); } writer.writeRecord(keys); _.forEach(objects, function(obj){ var row = []; _.forEach(obj, function(val,key){ row[idxs[key]] = val; }); writer.writeRecord(row); }); }